從0開發3D引擎(十一):使用領域驅動設計,從最小3D程式中提煉引擎(第二部分)
阿新 • • 發佈:2020-03-04
[TOC]
大家好,本文根據領域驅動設計的成果,開始實現從最小的3D程式中提煉引擎。
# 上一篇博文
[從0開發3D引擎(十):使用領域驅動設計,從最小3D程式中提煉引擎(第一部分)](https://www.cnblogs.com/chaogex/p/12408831.html)
# 本文流程
我們根據上文的成果,按照下面的步驟開始實現從最小的3D程式中提煉引擎:
1、建立程式碼的資料夾結構
2、index.html實現呼叫引擎API
3、根據用例圖和API層的設計,用虛擬碼實現index.html
4、按照index.html從上往下的API呼叫順序,依次實現API:setCanvasById、setClearColor、addGLSL、createTriangleVertexData、addTriangle、setCamera
# 回顧上文
上文的領域驅動設計獲得了下面的成果:
1、使用者邏輯和引擎邏輯
2、分層架構檢視和每一層的設計
3、領域驅動設計的戰略成果
1)引擎子域和限界上下文劃分
2)限界上下文對映圖
4、領域驅動設計的戰術成果
1)領域概念
2)領域檢視
5、資料檢視和PO的相關設計
6、一些細節的設計
7、基本的優化
# 解釋基本的操作
- 如何在瀏覽器上執行index.html
1、在TinyWonder專案根目錄上執行start命令:
```js
yarn start
```
2、在瀏覽器地址中輸入下面的url並回車,即可執行index.html頁面
```html
http://127.0.0.1:8080
```
# 開始實現
開啟最小3D程式的TinyWonder專案,現在我們開始具體實現。
## 準備
我們要完全重寫src/的內容,因此在專案根目錄上新建mine/資料夾,將src/資料夾拷貝mine/中,並清空src/資料夾。
通過備份src/資料夾,我們能容易地調出最小3D程式的程式碼供我們參考。
## 建立程式碼的資料夾結構,約定模組檔案的命名規則
### 模組檔案的命名原則
- 加上所屬層級/模組的字尾名
這是為了減少重名的機率
- 儘量簡潔
因此應該讓字尾名儘可能地短,只要幾乎不會出現重名的情況,那麼不僅可以省略一些層級/模組的字尾名,而且有些模組檔案甚至完全不加字尾名
### 一級和二級資料夾
如下圖所示:
![截圖2020-03-04上午8.28.11.png-16.5kB][1]
這是按照分層架構來劃分的資料夾:
- 一級資料夾(xxx_layer/)對應每個層級
- 二級資料夾(xxx_layer/的子資料夾)對應每層的物件
### api_layer的資料夾
api_layer/api/放置API模組檔案,如SceneJsAPI.re等
### application_layer的資料夾
application_layer/service/放置應用服務模組檔案,如SceneApService.re等
### domain_layer的資料夾
domain_layer/domain/放置領域服務、實體和值物件的模組檔案
domain_layer/repo/放置倉庫的模組檔案
domain/的子資料夾對應引擎的各個子域,如下圖所示:
![截圖2020-03-04上午8.33.04.png-15.4kB][2]
引擎子域資料夾的子資料夾對應該子域的限界上下文,如下圖所示:
![截圖2020-03-04上午8.34.32.png-32.2kB][3]
限界上下文資料夾的子檔案均為entity/、value_object/、service/,分別放置實體、值物件和領域服務的模組檔案。
部分截圖如下圖所示:
![截圖2020-03-04上午8.37.22.png-22.3kB][4]
entity/、value_object/、service/資料夾的模組檔案的命名規則分別為:
- 實體+限界上下文+Entity.re
如SceneSceneGraphEntity.re
- 值物件+限界上下文+VO.re
如TriangleSceneGraphVO.re
- 領域服務+限界上下文+DoService.re
如RenderRenderDoService.re
如果從這三個子資料夾的檔案中提出公共程式碼的模組檔案(如在後面,會從值物件ImmutableHashMap和值物件MutableHashMap中提出HashMap模組),則該模組檔案的命名規則為:
模組名+限界上下文.re
(如將HashMap模組檔案命名為HashMapContainer.re)
### infrastructure_layer的資料夾
infrastructure_layer/data/的資料夾結構如下圖所示:
![截圖2020-03-04上午8.40.45.png-9.6kB][5]
ContainerManager.re負責實現“容器管理”
container/放置PO Container相關的模組檔案
po/放置PO型別定義的檔案
infrastructure_layer/external/資料夾結構如下圖所示:
![截圖2020-03-04上午8.41.13.png-5.6kB][6]
external_object/放置外部物件的FFI檔案
library/放置js庫的FFI檔案
## index.html實現呼叫引擎API
index.html需要引入引擎檔案,呼叫它的API。
我們首先考慮的實現方案是:
與最小3D程式一樣,index.html以ES6 module的方式引入要使用的引擎的每個模組檔案(一個.re的引擎檔案就是一個模組檔案),呼叫暴露的API函式。
index.html的相關虛擬碼如下:
```js
```
這個方案有下面的缺點:
- 使用者訪問的許可權過大
使用者可以訪問非API的函式,如引擎的私有函式
- 使用者需要知道要呼叫的引擎API在引擎的哪個模組檔案中,以及模組檔案的路徑,這樣會增加使用者的負擔
如使用者需要知道setCanvasById在CameraJsAPI.js中,並且需要知道CameraJsAPI.js的路徑
- 瀏覽器需要支援ES6 module import
因此,我們使用下面的方案來實現,該方案可以解決上一個方案的缺點:
- 把引擎所有的API模組放到一個名稱空間中,讓使用者通過它來呼叫API
使用者只能訪問到API,從而讓引擎控制了使用者訪問許可權;
使用者只需要知道名稱空間和API模組的名字,減少了負擔。
- 使用webpack,將與引擎API相關的檔案打包成一個檔案,在index.html中引入該檔案
這樣瀏覽器就不需要支援ES6 module import了
我們通過下面的步驟來實現該方案:
1、建立gulp任務
該任務會建立src/Index.re檔案,它引用了引擎所有的API模組。
通過下面的子步驟來實現該任務:
1)在專案根目錄上,加入gulpfile.js檔案
2)在gulpfile.js中,加入gulp任務:generateIndex,該任務負責把引擎的所有API模組放到Index.re檔案中
3)實現generateIndex任務
因為我們希望用Reason而不是用js來實現,而且考慮到該任務屬於通用任務,多個Reason專案都需要它,所以我們通過下面的步驟來實現generateIndex任務:
a)建立新專案:TinyWonderGenerateIndex
b)進入新專案,用Reason程式碼來實現generateIndex任務的邏輯
其中src/Generate.re的generate函式是提供給使用者(如TinyWonder專案的generateIndex任務)使用的API,它的程式碼如下:
```re
let generate =
(
globCwd: string,
rootDir: string,
sourceFileGlobArr: array(string),
destDir: string,
config,
) => {
let excludeList = config##exclude |> Array.to_list;
let replaceAPIModuleNameFunc =
config##replaceAPIModuleNameFunc
|> Js.Option.getWithDefault(moduleName =>
moduleName |> Js.String.replace("JsAPI", "")
);
sourceFileGlobArr
|> Array.to_list
|> List.fold_left(
(fileDataList, filePath) => {
let fileName = Path.basename_ext(filePath, ".re");
[
syncWithConfig(Path.join([|rootDir, filePath|]), {"cwd": globCwd})
|> Array.to_list
|> List.filter(filePath =>
excludeList
|> List.filter(exclude =>
filePath |> Js.String.includes(exclude)
)
|> List.length === 0
)
|> List.map(filePath =>
(
Path.basename_ext(filePath, ".re")
|> replaceAPIModuleNameFunc,
Path.basename_ext(filePath, ".re"),
Fs.readFileAsUtf8Sync(filePath) |> _findPublicFunctionList,
)
),
...fileDataList,
];
},
[],
)
|> List.flatten
|> _buildContent
|> _writeToIndexFile(destDir);
};
```
該函式使用glob庫遍歷sourceFileGlobArr陣列,將“globCwd + rootDir + sourceFileGlob”路徑中的所有Reason檔案的函式引入到destDir/Index.re的對應的模組中。
可在config.exclude中定義要排除的檔案路徑的陣列,在config.replaceAPIModuleNameFunc函式中定義如何重新命名模組名。
該專案的完整程式碼地址為:[Tiny-Wonder-GenerateIndex](https://github.com/Wonder-Book/Book-Tiny-Wonder-GenerateIndex)
舉個例子來說明使用者如何使用generate函式:
假設使用者工作在TinyWonder專案(專案根目錄為/y/Github/TinyWonder/)上,建立了./src/api/AJsAPI.re,它的程式碼為:
```re
let aFunc = v => 1;
```
它的模組名為“AJsAPI”。
使用者還建立了./src/api/ddd/BJsAPI.re,它的程式碼為:
```re
let bFunc = v => v * 2;
```
它的模組名為“BJsAPI”
使用者可以在TinyWonder專案根目錄上,用js程式碼呼叫Tiny-Wonder-GenerateIndex專案的generate函式:
```js
//將路徑為“/y/Github/TinyWonder/src/**/api/**/*.re”(排除包含“src/Config”的檔案路徑)的所有API模組引入到./src/Index.re中
generate("/", "/y/Github/TinyWonder/src", ["**/api/**/*.re"], "./src/", {
exclude: ["src/Config"],
//該函式用於去掉API模組名的“JsAPI”,如將“AJsAPI”重新命名為“A”
replaceAPIModuleNameFunc: (moduleName) => moduleName.replace("JsAPI", "")
})
```
使用者呼叫後,會在./src/中加入Index.re檔案,它的程式碼如下:
```re
module A = {
let aFunc = AJsAPI.aFunc;
};
module B = {
let bFunc = BJsAPI.bFunc;
};
```
現在我們清楚瞭如何使用generate函式,那麼繼續在TinyWonderGenerateIndex專案上完成剩餘工作:
c)編譯該專案的Reason程式碼為Commonjs模組規範的js檔案
d)通過npm釋出該專案
package.json需要定義main欄位:
```json
"main": "./lib/js/src/Generate.js",
```
e)回到TinyWonder專案
f)在TinyWonder專案的gulpfile.js->generateIndex任務中,引入TinyWonderGenerateIndex專案,呼叫它的generate函式
TinyWonder專案的相關程式碼為:
gulpfile.js
```js
var gulp = require("gulp");
var path = require("path");
gulp.task("generateIndex", function (done) {
var generate = require("tiny-wonder-generate-index");
var rootDir = path.join(process.cwd(), "src"),
destDir = "./src/";
generate.generate("/", rootDir, ["**/api/**/*.re"], destDir, {
exclude: [],
replaceAPIModuleNameFunc: (moduleName) => moduleName.replace("JsAPI", "")
});
done();
});
```
2、建立gulp任務後,我們需要引入webpack,它將Index.re關聯的引擎檔案打包成一個檔案
1)在專案的根目錄上,加入webpack.config.js檔案:
```js
const path = require('path');
const isProd = process.env.NODE_ENV === 'production';
module.exports = {
entry: {
"wd": "./lib/es6_global/src/Index.js"
},
mode: isProd ? 'production' : 'development',
output: {
filename: '[name].js',
path: path.join(__dirname, "dist"),
library: 'wd',
libraryTarget: 'umd'
},
target: "web"
};
```
2)在package.json中,加入script:
```json
"scripts": {
...
"webpack:dev": "NODE_ENV=development webpack --config webpack.config.js",
"webpack": "gulp generateIndex && npm run webpack:dev"
}
```
3、執行測試
1)在src/api_layer/api/中加入一個用於測試的API模組檔案:TestJsAPI.re,定義兩個API函式:
```re
let aFunc = v => Js.log(v);
let bFunc = v => Js.log(v * 2);
```
2)安裝gulp後,在專案根目錄上執行generateIndex任務,進行執行測試:
```js
gulp generateIndex
```
可以看到src/中加入了Index.re檔案,Index.re程式碼為:
```re
module Test = {
let aFunc = TestJsAPI.aFunc;
let bFunc = TestJsAPI.bFunc;
};
```
3)在安裝webpack後,在專案根目錄上執行下面命令,打包為dist/wd.js檔案,其中名稱空間為“wd”:
```js
yarn webpack
```
4)index.html引入wd.js檔案,呼叫引擎API
index.html的程式碼為:
```html
```
5)在瀏覽器上執行index.html,開啟控制檯,可以看到列印了"1"和“4”
## 用虛擬碼實現index.html
我們根據用例圖和設計的API來實現index.html:
index.html需要使用最小3D程式的資料,來實現用例圖中“index.html”角色包含的使用者邏輯;
index.html需要呼叫引擎API,來實現用例圖中的用例。
index.html的虛擬碼實現如下所示:
```html
```
我們按照下面的步驟來具體實現index.html:
1、按照index.html從上往下的API呼叫順序,確定要實現的API
2、按照引擎的層級,從上層的API開始,實現每一層的對應模組
3、實現index.html的相關程式碼
4、執行測試
現在我們按照順序,確定要實現API->“CanvasJsAPI.setCanvasById” 。
現在來實現該API:
## 實現“CanvasJsAPI.setCanvasById”
1、在src/api_layer/api/中加入CanvasJsAPI.re,實現API
CanvasJsAPI.re程式碼為:
```re
//因為應用服務CanvasApService的setCanvasById函式輸入和輸出的DTO與這裡(API)的setCanvasById函式輸入和輸出的VO相同,所以不需要在兩者之間轉換
let setCanvasById = CanvasApService.setCanvasById;
```
2、在src/application_layer/service/中加入CanvasApService.re,實現應用服務
CanvasApService.re程式碼為:
```re
let setCanvasById = canvasId => {
CanvasCanvasEntity.setCanvasById(canvasId);
//列印canvasId,用於執行測試
Js.log(canvasId);
};
```
3、把最小3D程式的DomExtend.re放到src/instracture_layer/external/external_object/中,刪除目前沒用到的程式碼(刪除requestAnimationFrame FFI)
DomExtend.re程式碼為:
```re
type htmlElement = {
.
"width": int,
"height": int,
};
type body;
type document = {. "body": body};
[@bs.val] external document: document = "";
[@bs.send] external querySelector: (document, string) => htmlElement = "";
```
4、在src/domain_layer/domain/canvas/canvas/entity/中加入CanvasCanvasEntity.re,建立聚合根Canvas
CanvasCanvasEntity.re程式碼為:
```re
//這裡定義Canvas DO的型別(Canvas DO為一個畫布)
type t = DomExtend.htmlElement;
let setCanvasById = canvasId =>
Repo.setCanvas(
DomExtend.querySelector(DomExtend.document, {j|#$canvasId|j}),
);
```
5、因為需要使用Result來處理錯誤,所以加入值物件Result
1)在領域檢視的“容器”限界上下文中,加入值物件Result,負責操作錯誤處理的容器Result
2)在src/domain_layer/domain/structure/container/value_object/中加入ResultContainerVO.re,建立值物件Result
ResultContainerVO.re程式碼為:
```re
type t('a, 'b) =
| Success('a)
| Fail('b);
let succeed = x => Success(x);
let fail = x => Fail(x);
let _raiseErrorAndReturn = msg => Js.Exn.raiseError(msg);
let failWith = x => (x |> _raiseErrorAndReturn)->Fail;
let either = (successFunc, failureFunc, twoTrackInput) =>
switch (twoTrackInput) {
| Success(s) => successFunc(s)
| Fail(f) => failureFunc(f)
};
let bind = (switchFunc, twoTrackInput) =>
either(switchFunc, fail, twoTrackInput);
let tap = (oneTrackFunc, twoTrackInput) =>
either(
result => {
result |> oneTrackFunc |> ignore;
result |> succeed;
},
fail,
twoTrackInput,
);
let tryCatch = (oneTrackFunc: 'a => 'b, x: 'a): t('b, Js.Exn.t) =>
try(oneTrackFunc(x) |> succeed) {
| Js.Exn.Error(e) => fail(e)
| err => {j|unknown error: $err|j} |> _raiseErrorAndReturn |> fail
};
let mapSuccess = (mapFunc, result) =>
switch (result) {
| Success(s) => mapFunc(s) |> succeed
| Fail(f) => fail(f)
};
let handleFail = (handleFailFunc: 'f => unit, result: t('s, 'f)): unit =>
switch (result) {
| Success(s) => ()
| Fail(f) => handleFailFunc(f)
};
```
6、在src/infrastructure_layer/data/po/中加入POType.re,定義PO的型別,目前PO只包含Canvas PO
POType.re程式碼為:
```re
//因為在建立PO時,Canvas PO(一個畫布)並不存在,所以它為option型別
type po = {canvas: option(CanvasPOType.canvas)};
```
7、在src/infrastructure_layer/data/po/中加入CanvasPOType.re,定義Canvas PO型別
CanvasPOType.re程式碼為:
```re
type canvas = DomExtend.htmlElement;
```
8、因為定義了option型別,所以加入領域服務Option
1)在領域檢視的“容器”限界上下文中,加入領域服務Option,負責操作Option容器
2)在src/domain_layer/domain/structure/container/service/中加入OptionContainerDoService.re,建立領域服務Option
OptionContainerDoService.re程式碼為:
```re
//如果optionData為Some(v),返回v;否則丟擲異常
let unsafeGet = optionData => optionData |> Js.Option.getExn;
//通過使用Result,安全地取出optionData的值
let get = optionData => {
switch (optionData) {
| None => ResultContainerVO.failWith({|data not exist(get by getExn)|})
| Some(data) => ResultContainerVO.succeed(data)
};
};
```
9、在src/domain_layer/repo/中加入Repo.re,實現倉庫對Canvas PO的操作
Repo.re程式碼為:
```re
let getCanvas = () => {
let po = ContainerManager.getPO();
po.canvas;
};
let setCanvas = canvas => {
let po = ContainerManager.getPO();
{...po, canvas: Some(canvas)} |> ContainerManager.setPO;
};
```
10、在src/infrastructure_layer/data/中加入ContainerManager.re,實現PO Container中PO的讀寫
ContainerManager.re程式碼為:
```re
let getPO = () => {
Container.poContainer.po;
};
let setPO = po => {
Container.poContainer.po = po;
};
```
11、在src/infrastructure_layer/data/container/中加入ContainerType.re和Container.re,分別定義PO Container的型別和建立PO Container
ContainerType.re程式碼為:
```re
type poContainer = {mutable po: POType.po};
```
Container.re程式碼為:
```re
let poContainer: ContainerType.poContainer = {
po: CreateRepo.create(),
};
```
12、在src/domain_layer/repo/中加入CreateRepo.re,實現建立PO
CreateRepo.re程式碼為:
```re
open POType;
let create = () => {canvas: None};
```
13、在專案根目錄上執行webpack命令,更新wd.js檔案
```js
yarn webpack
```
14、實現index.html相關程式碼
index.html程式碼為:
```html
use engine
```
15、執行測試
執行index.html頁面
開啟控制檯,可以看到列印了"webgl"
## 實現“GraphicsJsAPI.setClearColor”
1、在src/api_layer/api/中加入GraphicsJsAPI.re,實現API
GraphicsJsAPI.re程式碼為:
```re
let setClearColor = GraphicsApService.setClearColor;
```
2、在src/application_layer/service/中加入GraphicsApService.re,實現應用服務
GraphicsApService.re程式碼為:
```re
let setClearColor = clearColor => {
ContextContextEntity.setClearColor(Color4ContainerVO.create(clearColor));
//用於執行測試
Js.log(clearColor);
};
```
3、在src/domain_layer/domain/structure/container/value_object/中加入Color4ContainerVO.re,建立值物件Color4
Color4ContainerVO.re程式碼為:
```re
type r = float;
type g = float;
type b = float;
type a = float;
type t =
| Color4(r, g, b, a);
let create = ((r, g, b, a)) => Color4(r, g, b, a);
let value = color =>
switch (color) {
| Color4(r, g, b, a) => (r, g, b, a)
};
```
4、在src/domain_layer/domain/webgl_context/context/entity/中加入ContextContextEntity.re,建立聚合根Context
ContextContextEntity.re程式碼為:
```re
let setClearColor = clearColor => {
ContextRepo.setClearColor(clearColor);
};
```
5、在src/infrastructure_layer/data/po/中加入ContextPOType.re,定義Context PO型別
ContextPOType.re程式碼為:
```re
type context = {clearColor: (float, float, float, float)};
```
6、修改POType.re
POType.re相關程式碼為:
```re
type po = {
...
context: ContextPOType.context,
};
```
7、在src/domain_layer/repo/中加入ContextRepo.re,實現倉庫對Context PO的clearColor欄位的操作
ContextRepo.re程式碼為:
```re
let getClearColor = () => {
Repo.getContext().clearColor;
};
let setClearColor = clearColor => {
Repo.setContext({
...Repo.getContext(),
clearColor: Color4ContainerVO.value(clearColor),
});
};
```
8、修改Repo.re,實現倉庫對Context PO的操作
Repo.re相關程式碼為:
```re
let getContext = () => {
let po = ContainerManager.getPO();
po.context;
};
let setContext = context => {
let po = ContainerManager.getPO();
{...po, context} |> ContainerManager.setPO;
};
```
9、修改CreateRepo.re,實現建立Context PO
CreateRepo.re相關程式碼為:
```re
let create = () => {
...
context: {
clearColor: (0., 0., 0., 1.),
},
};
```
10、在專案根目錄上執行webpack命令,更新wd.js檔案
```js
yarn webpack
```
11、實現index.html相關程式碼
index.html程式碼為:
```html
```
12、執行測試
執行index.html頁面
開啟控制檯,可以看到列印了陣列:[0,0,0,1]
## 實現“ShaderJsAPI.addGLSL”
1、在src/api_layer/api/中加入ShaderJsAPI.re,實現API
ShaderJsAPI.re程式碼為:
```re
let addGLSL = ShaderApService.addGLSL;
```
2、設計領域模型ShaderManager、Shader、GLSL的DO
根據領域模型:
![此處輸入圖片的描述][7]
和識別的引擎邏輯:
獲得所有Shader的Shader名稱和GLSL組集合
我們可以設計聚合根ShaderManager的DO為集合list:
```re
type t = {glsls: list(Shader DO)};
```
設計值物件GLSL的DO為:
```re
type t =
| GLSL(string, string);
```
設計實體Shader的DO為:
```re
type shaderName = string;
type t =
| Shader(shaderName, GLSL DO);
```
3、在src/application_layer/service/中加入ShaderApService.re,實現應用服務
ShaderApService.re程式碼為:
```re
let addGLSL = (shaderName, glsl) => {
ShaderManagerShaderEntity.addGLSL(
ShaderShaderEntity.create(shaderName, GLSLShaderVO.create(glsl)),
);
//用於執行測試
Js.log((shaderName, glsl));
};
```
4、在src/domain_layer/domain/shader/shader/entity/中加入ShaderShaderEntity.re,建立實體Shader
ShaderShaderEntity.re程式碼為:
```re
type shaderName = string;
type t =
| Shader(shaderName, GLSLShaderVO.t);
let create = (shaderName, glsl) => Shader(shaderName, glsl);
let getShaderName = shader =>
switch (shader) {
| Shader(shaderName, glsl) => shaderName
};
let getGLSL = shader =>
switch (shader) {
| Shader(shaderName, glsl) => glsl
};
```
5、在src/domain_layer/domain/shader/shader/value_object/中加入GLSLShaderVO.re,建立值物件GLSL
GLSLShaderVO.re程式碼為:
```re
type t =
| GLSL(string, string);
let create = ((vs, fs)) => GLSL(vs, fs);
let value = glsl =>
switch (glsl) {
| GLSL(vs, fs) => (vs, fs)
};
```
6、在src/domain_layer/domain/shader/shader/entity/中加入ShaderManagerShaderEntity.re,建立聚合根ShaderManager
ShaderManagerShaderEntity.re程式碼為:
```re
type t = {glsls: list(ShaderShaderEntity.t)};
let addGLSL = shader => {
ShaderManagerRepo.addGLSL(shader);
};
```
7、在src/infrastructure_layer/data/po/中加入ShaderManagerPOType.re,定義ShaderManager PO的型別
ShaderManagerPOType.re程式碼為:
```re
//shaderId就是Shader的名稱
type shaderId = string;
type shaderManager = {glsls: list((shaderId, (string, string)))};
```
8、修改POType.re
POType.re相關程式碼為:
```re
type po = {
...
shaderManager: ShaderManagerPOType.shaderManager,
};
```
9、在src/domain_layer/repo/中加入ShaderManagerRepo.re,實現倉庫對ShaderManager PO的glsls欄位的操作
ShaderManagerRepo.re程式碼為:
```re
open ShaderManagerPOType;
let _getGLSLs = ({glsls}) => glsls;
let addGLSL = shader => {
Repo.setShaderManager({
...Repo.getShaderManager(),
glsls: [
(
ShaderShaderEntity.getShaderName(shader),
shader |> ShaderShaderEntity.getGLSL |> GLSLShaderVO.value,
),
..._getGLSLs(Repo.getShaderManager()),
],
});
};
```
10、修改Repo.re,實現倉庫對ShaderManager PO的操作
Repo.re相關程式碼為:
```re
let getShaderManager = () => {
let po = ContainerManager.getPO();
po.shaderManager;
};
let setShaderManager = shaderManager => {
let po = ContainerManager.getPO();
{...po, shaderManager} |> ContainerManager.setPO;
};
```
11、修改CreateRepo.re,實現建立ShaderManager PO
CreateRepo.re相關程式碼為:
```re
let create = () => {
...
shaderManager: {
glsls: [],
},
};
```
12、在專案根目錄上執行webpack命令,更新wd.js檔案
```js
yarn webpack
```
13、實現index.html相關程式碼
index.html程式碼為:
```html
```
14、執行測試
執行index.html頁面
開啟控制檯,可以看到列印了兩個Shader的資料:
![截圖2020-02-29下午12.13.07.png-78.1kB][8]
## 實現“SceneJsAPI.createTriangleVertexData”
1、在src/api_layer/api/中加入SceneJsAPI.re,實現API
SceneJsAPI.re程式碼為:
```re
let createTriangleVertexData = SceneApService.createTriangleVertexData;
```
2、在src/application_layer/service/中加入SceneApService.re,實現應用服務
SceneApService.re程式碼為:
```re
let createTriangleVertexData = () => {
//vertices和indices為DO資料,分別為值物件Vertices的DO和值物件Indices的DO
let (vertices, indices) = GeometrySceneGraphVO.createTriangleVertexData();
//將DO轉成DTO
let data = (
vertices |> VerticesSceneGraphVO.value,
indices |> IndicesSceneGraphVO.value,
);
//用於執行測試
Js.log(data);
//將DTO返回給API層
data;
};
```
3、在src/domain_layer/domain/scene/scene_graph/value_object/中加入GeometrySceneGraphVO.re,建立值物件Geometry
GeometrySceneGraphVO.re程式碼為:
```re
let createTriangleVertexData = () => {
open Js.Typed_array;
let vertices =
Float32Array.make([|0., 0.5, 0.0, (-0.5), (-0.5), 0.0, 0.5, (-0.5), 0.0|])
|> VerticesSceneGraphVO.create;
let indices = Uint16Array.make([|0, 1, 2|]) |> IndicesSceneGraphVO.create;
(vertices, indices);
};
```
4、在src/domain_layer/domain/scene/scene_graph/value_object/中加入VerticesSceneGraphVO.re和IndicesSceneGraphVO.re,建立值物件Vertices和值物件Indices
VerticesSceneGraphVO.re程式碼為:
```re
open Js.Typed_array;
type t =
| Vertices(Float32Array.t);
let create = value => Vertices(value);
let value = vertices =>
switch (vertices) {
| Vertices(value) => value
};
```
IndicesSceneGraphVO.re程式碼為:
```re
open Js.Typed_array;
type t =
| Indices(Uint16Array.t);
let create = value => Indices(value);
let value = indices =>
switch (indices) {
| Indices(value) => value
};
```
5、在專案根目錄上執行webpack命令,更新wd.js檔案
```js
yarn webpack
```
6、實現index.html相關程式碼
index.html程式碼為:
```html
```
7、執行測試
執行index.html頁面
開啟控制檯,可以看到列印了三次頂點資料
## 實現“SceneJsAPI.addTriangle”
1、修改SceneJsAPI.re,實現API
SceneJsAPI.re相關程式碼為:
```re
let addTriangle = (position, (vertices, indices), (shaderName, colors)) => {
//這裡的VO與DTO有區別:VO的colors的型別為array,而DTO的colors的型別為list,所以需要將colors的array轉換為list
SceneApService.addTriangle(
position,
(vertices, indices),
(shaderName, colors |> Array.to_list),
);
};
```
2、設計聚合根Scene、值物件Triangle和它所有的值物件的DO
根據領域模型:
![此處輸入圖片的描述][9]
我們按照Scene的聚合關係,從下往上開始設計:
設計值物件Vector的DO為:
```re
type t =
| Vector(float, float, float);
```
設計值物件Position的DO為:
```re
type t =
| Position(Vector.t);
```
設計值物件Vertices的DO為:
```re
open Js.Typed_array;
type t =
| Vertices(Float32Array.t);
```
設計值物件Indices的DO為:
```re
open Js.Typed_array;
type t =
| Indices(Uint16Array.t);
```
設計值物件Color3的DO為:
```re
type r = float;
type g = float;
type b = float;
type t =
| Color3(r, g, b);
```
設計值物件Transform的DO為:
```re
type t = {position: Position DO};
```
設計值物件Geometry的DO為:
```re
type t = {
vertices: Vertices DO,
indices: Indices DO,
};
```
對於值物件Material的DO,我們需要思考:
在領域模型中,Material組合了一個Shader,這應該如何體現到Material的DO中?
解決方案:
1)將Shader DO的Shader名稱和值物件GLSL拆開
2)Shader DO只包含Shader名稱,它即為實體Shader的id
3)Material DO包含一個Shader的id
這樣就使Material通過Shader的id(Shader名稱),與Shader關聯起來了!
因為Shader DO移除了值物件GLSL,所以我們需要重寫與Shader相關的程式碼:
1)重寫ShaderShaderEntity.re
ShaderShaderEntity.re程式碼為:
```re
type shaderName = string;
type id = shaderName;
type t =
| Shader(id);
let create = id => Shader(id);
let getId = shader =>
switch (shader) {
| Shader(id) => id
};
```
2)重寫ShaderManagerShaderEntity.re
ShaderManagerShaderEntity.re程式碼為:
```re
type t = {glsls: list((ShaderShaderEntity.t, GLSLShaderVO.t))};
let addGLSL = (shader, glsl) => {
ShaderManagerRepo.addGLSL(shader, glsl);
};
```
3)重寫ShaderApService.re
ShaderApService.re程式碼為:
```re
let addGLSL = (shaderName, glsl) => {
ShaderManagerShaderEntity.addGLSL(
ShaderShaderEntity.create(shaderName),
GLSLShaderVO.create(glsl),
);
//用於執行測試
Js.log((shaderName, glsl));
};
```
4)重寫ShaderManagerRepo.re
ShaderManagerRepo.re程式碼為:
```re
open ShaderManagerPOType;
let _getGLSLs = ({glsls}) => glsls;
let addGLSL = (shader, glsl) => {
Repo.setShaderManager({
...Repo.getShaderManager(),
glsls: [
(ShaderShaderEntity.getId(shader), GLSLShaderVO.value(glsl)),
..._getGLSLs(Repo.getShaderManager()),
],
});
};
```
現在我們可以設計值物件Material的DO為:
```re
type t = {
shader: Shader DO,
colors: list(Color3 DO),
};
```
注意:這裡的欄位名是“shader”而不是“shaderName”或者“shaderId”,因為這樣才能直接體現Material組合了一個Shader,而不是組合了一個Shader名稱或Shader id
我們繼續設計,設計值物件Triangle的DO為:
```re
type t = {
transform: Transform DO,
geometry: Geometry DO,
material: Material DO,
};
```
設計聚合根Scene的DO為:
```re
type t = {triangles: list(Triangle DO)};
```
3、修改SceneApService.re,實現應用服務
SceneApService.re相關程式碼為:
```re
let addTriangle = (position, (vertices, indices), (shaderName, colors)) => {
SceneSceneGraphEntity.addTriangle(
position |> VectorContainerVO.create |> PositionSceneGraphVO.create,
(
VerticesSceneGraphVO.create(vertices),
IndicesSceneGraphVO.create(indices),
),
(
ShaderShaderEntity.create(shaderName),
colors |> List.map(color => Color3ContainerVO.create(color)),
),
);
//用於執行測試
Js.log(Repo.getScene());
};
```
4、加入值物件Triangle和它的所有值物件
1)在src/domain_layer/domain/structure/math/value_object/中加入VectorMathVO.re,建立值物件Vector
VectorMathVO.re程式碼為:
```re
type t =
| Vector(float, float, float);
let create = ((x, y, z)) => Vector(x, y, z);
let value = vec =>
switch (vec) {
| Vector(x, y, z) => (x, y, z)
};
```
2)在src/domain_layer/domain/scene/scene_graph/value_object/中加入PositionSceneGraphVO.re,建立值物件Position
PositionSceneGraphVO.re程式碼為:
```re
type t =
| Position(VectorMathVO.t);
let create = value => Position(value);
let value = position =>
switch (position) {
| Position(pos) => pos
};
```
3)在src/domain_layer/domain/structure/container/value_object/中加入Color3ContainerVO.re,建立值物件Color3
Color3ContainerVO.re程式碼為:
```re
type r = float;
type g = float;
type b = float;
type t =
| Color3(r, g, b);
let create = ((r, g, b)) => Color3(r, g, b);
let value = color =>
switch (color) {
| Color3(r, g, b) => (r, g, b)
};
```
4)在src/domain_layer/domain/scene/scene_graph/value_object/中加入TransformSceneGraphVO.re,建立值物件Transform
TransformSceneGraphVO.re程式碼為:
```re
type t = {position: PositionSceneGraphVO.t};
```
5)修改GeometrySceneGraphVO.re,定義DO
GeometrySceneGraphVO.re相關程式碼為:
```re
type t = {
vertices: VerticesSceneGraphVO.t,
indices: IndicesSceneGraphVO.t,
};
```
6)在src/domain_layer/domain/scene/scene_graph/value_object/中加入MaterialSceneGraphVO.re,建立值物件Material
MaterialSceneGraphVO.re程式碼為:
```re
type t = {
shader: ShaderShaderEntity.t,
colors: list(Color3ContainerVO.t),
};
```
7)在src/domain_layer/domain/scene/scene_graph/value_object/中加入TriangleSceneGraphVO.re,建立值物件Triangle
TriangleSceneGraphVO.re程式碼為:
```re
type t = {
transform: TransformSceneGraphVO.t,
geometry: GeometrySceneGraphVO.t,
material: MaterialSceneGraphVO.t,
};
```
5、在src/domain_layer/domain/scene/scene_graph/value_object/中加入SceneSceneGraphEntity.re,建立聚合根Scene
SceneSceneGraphEntity.re程式碼為:
```re
type t = {triangles: list(TriangleSceneGraphVO.t)};
let addTriangle = (position, (vertices, indices), (shader, colors)) => {
SceneRepo.addTriangle(position, (vertices, indices), (shader, colors));
};
```
6、在src/infrastructure_layer/data/po/中加入ScenePOType.re,定義Scene PO的型別
ScenePOType.re程式碼為:
```re
type transform = {position: (float, float, float)};
type geometry = {
vertices: Js.Typed_array.Float32Array.t,
indices: Js.Typed_array.Uint16Array.t,
};
type material = {
shader: string,
colors: list((float, float, float)),
};
type triangle = {
transform,
geometry,
material,
};
type scene = {triangles: list(triangle)};
```
7、修改POType.re
POType.re相關程式碼為:
```re
type po = {
...
scene: ScenePOType.scene,
};
```
8、實現Scene相關的倉庫
我們按照倉庫依賴關係,從上往下開始實現:
1)建立資料夾src/domain_layer/repo/scene/
2)在src/domain_layer/repo/scene/中加入SceneRepo.re,實現倉庫對Scene PO的triangles欄位的操作
ShaderManagerRepo.re程式碼為:
```re
open ScenePOType;
let _getTriangles = ({triangles}) => triangles;
let addTriangle = (position, (vertices, indices), (shader, colors)) => {
Repo.setScene({
...Repo.getScene(),
triangles: [
TriangleSceneRepo.create(
TransformSceneRepo.create(position),
GeometrySceneRepo.create(vertices, indices),
MaterialSceneRepo.create(shader, colors),
),
..._getTriangles(Repo.getScene()),
],
});
};
```
3)在src/domain_layer/repo/scene/中加入TrianglerSceneRepo.re,實現建立Scene PO的一個Triangle資料
TriangleSceneRepo.re程式碼為:
```re
open ScenePOType;
let create = (transform, geometry, material) => {
transform,
geometry,
material,
};
```
4)在src/domain_layer/repo/scene/中加入TransformSceneRepo.re,實現建立Scene PO的一個Triangle的Transform資料
TransformSceneRepo.re程式碼為:
```re
open ScenePOType;
let create = position => {
position: position |> PositionSceneGraphVO.value |> VectorMathVO.value,
};
```
5)在src/domain_layer/repo/scene/中加入GeometrySceneRepo.re,實現建立Scene PO的一個Triangle的Geometry資料
GeometrySceneRepo.re程式碼為:
```re
open ScenePOType;
let create = (vertices, indices) => {
vertices: vertices |> VerticesSceneGraphVO.value,
indices: indices |> IndicesSceneGraphVO.value,
};
```
6)在src/domain_layer/repo/scene/中加入MaterialSceneRepo.re,實現建立Scene PO的一個Triangle的Material資料
MaterialSceneRepo.re程式碼為:
```re
open ScenePOType;
let create = (shader, colors) => {
shader: shader |> ShaderShaderEntity.getId,
colors: colors |> List.map(color => {color |> Color3ContainerVO.value}),
};
```
9、修改Repo.re,實現倉庫對Scene PO的操作
Repo.re相關程式碼為:
```re
let getScene = () => {
let po = ContainerManager.getPO();
po.scene;
};
let setScene = scene => {
let po = ContainerManager.getPO();
{...po, scene} |> ContainerManager.setPO;
};
```
10、修改CreateRepo.re,實現建立Scene PO
CreateRepo.re相關程式碼為:
```re
let create = () => {
...
scene: {
triangles: [],
},
};
```
11、在專案根目錄上執行webpack命令,更新wd.js檔案
```js
yarn webpack
```
12、實現index.html相關程式碼
index.html程式碼為:
```html
```
13、執行測試
執行index.html頁面
開啟控制檯,可以看到列印了三次Scene PO的資料
## 實現“SceneJsAPI.setCamera”
1、修改SceneJsAPI.re,實現API
SceneJsAPI.re相關程式碼為:
```re
let setCamera = SceneApService.setCamera;
```
2、修改SceneApService.re,實現應用服務
SceneApService.re相關程式碼為:
```re
let setCamera = ((eye, center, up), (near, far, fovy, aspect)) => {
SceneSceneGraphEntity.setCamera(
(
EyeSceneGraphVO.create(eye),
CenterSceneGraphVO.create(center),
UpSceneGraphVO.create(up),
),
(
NearSceneGraphVO.create(near),
FarSceneGraphVO.create(far),
FovySceneGraphVO.create(fovy),
AspectSceneGraphVO.create(aspect),
),
);
//用於執行測試
Js.log(Repo.getScene());
};
```
3、加入Camera的所有值物件
1)在src/domain_layer/domain/scene/scene_graph/value_object/中加入EyeSceneGraphVO.re、CenterSceneGraphVO.re、UpSceneGraphVO.re,建立值物件Eye、Center、Up
EyeSceneGraphVO.re程式碼為:
```re
type t =
| Eye(VectorMathVO.t);
let create = value => Eye(value);
let value = eye =>
switch (eye) {
| Eye(value) => value
};
```
CenterSceneGraphVO.re程式碼為:
```re
type t =
| Center(VectorMathVO.t);
let create = value => Center(value);
let value = center =>
switch (center) {
| Center(value) => value
};
```
UpSceneGraphVO.re程式碼為:
```re
type t =
| Up(VectorMathVO.t);
let create = value => Up(value);
let value = up =>
switch (up) {
| Up(value) => value
};
```
2)在src/domain_layer/domain/scene/scene_graph/value_object/中加入NearSceneGraphVO.re、FarSceneGraphVO.re、FovySceneGraphVO.re、AspectSceneGraphVO.re,建立值物件Near、Far、Fovy、Aspect
NearSceneGraphVO.re程式碼為:
```re
type t =
| Near(float);
let create = value => Near(value);
let value = near =>
switch (near) {
| Near(value) => value
};
```
FarSceneGraphVO.re程式碼為:
```re
type t =
| Far(float);
let create = value => Far(value);
let value = far =>
switch (far) {
| Far(value) => value
};
```
FovySceneGraphVO.re程式碼為:
```re
type t =
| Fovy(float);
let create = value => Fovy(value);
let value = fovy =>
switch (fovy) {
| Fovy(value) => value
};
```
AspectSceneGraphVO.re程式碼為:
```re
type t =
| Aspect(float);
let create = value => Aspect(value);
let value = aspect =>
switch (aspect) {
| Aspect(value) => value
};
```
4、在src/domain_layer/domain/scene/scene_graph/value_object/中加入CameraSceneGraphVO.re,建立值物件Camera
CameraSceneGraphVO.re程式碼為:
```re
type t = {
eye: EyeSceneGraphVO.t,
center: CenterSceneGraphVO.t,
up: UpSceneGraphVO.t,
near: NearSceneGraphVO.t,
far: FarSceneGraphVO.t,
fovy: FovySceneGraphVO.t,
aspect: AspectSceneGraphVO.t,
};
```
5、修改SceneSceneGraphEntity.re,將Camera DO作為Scene DO 的camera欄位的資料,並實現setCamera函式:
SceneSceneGraphEntity.re相關程式碼為:
```re
type t = {
...
camera: option(CameraSceneGraphVO.t),
};
...
let setCamera = ((eye, center, up), (near, far, fovy, aspect)) => {
SceneRepo.setCamera((eye, center, up), (near, far, fovy, aspect));
};
```
6、修改ScenePOType.re,加入Scene PO的camera欄位的資料型別
ScenePOType.re相關程式碼為:
```re
type camera = {
eye: (float, float, float),
center: (float, float, float),
up: (float, float, float),
near: float,
far: float,
fovy: float,
aspect: float,
};
type scene = {
...
camera: option(camera),
};
```
7、實現Scene->Camera相關的倉庫
1)在src/domain_layer/repo/scene/中加入CameraSceneRepo.re,實現建立Scene PO的一個Camera資料
CameraSceneRepo.re程式碼為:
```re
open ScenePOType;
let create = ((eye, center, up), (near, far, fovy, aspect)) => {
eye: eye |> EyeSceneGraphVO.value |> VectorMathVO.value,
center: center |> CenterSceneGraphVO.value |> VectorMathVO.value,
up: up |> UpSceneGraphVO.value |> VectorMathVO.value,
near: NearSceneGraphVO.value(near),
far: FarSceneGraphVO.value(far),
fovy: FovySceneGraphVO.value(fovy),
aspect: AspectSceneGraphVO.value(aspect),
};
```
2)修改SceneRepo.re,實現倉庫對Scene PO的camera欄位的操作
SceneRepo.re相關程式碼為:
```re
let setCamera = ((eye, center, up), (near, far, fovy, aspect)) => {
Repo.setScene({
...Repo.getScene(),
camera:
Some(
CameraSceneRepo.create(
(eye, center, up),
(near, far, fovy, aspect),
),
),
});
};
```
8、修改CreateRepo.re,實現建立Scene PO的camera欄位
CreateRepo.re相關程式碼為:
```re
let create = () => {
...
scene: {
...
camera: None,
},
};
```
9、在專案根目錄上執行webpack命令,更新wd.js檔案
```js
yarn webpack
```
10、實現index.html相關程式碼
index.html程式碼為:
```html
```
11、執行測試
執行index.html頁面
開啟控制檯,可以看到列印了一次Scene PO的資料,它包含Camera的資料
[1]: http://static.zybuluo.com/yangyc/n54aibdn5chk2950lahs01wd/%E6%88%AA%E5%B1%8F2020-03-04%E4%B8%8A%E5%8D%888.28.11.png
[2]: http://static.zybuluo.com/yangyc/hzt5w04did1ms585swcgrw3z/%E6%88%AA%E5%B1%8F2020-03-04%E4%B8%8A%E5%8D%888.33.04.png
[3]: http://static.zybuluo.com/yangyc/e0gsmzockqsa1qlxgqr86wz5/%E6%88%AA%E5%B1%8F2020-03-04%E4%B8%8A%E5%8D%888.34.32.png
[4]: http://static.zybuluo.com/yangyc/o2lu2gs59w4rkh2t80m5wsw3/%E6%88%AA%E5%B1%8F2020-03-04%E4%B8%8A%E5%8D%888.37.22.png
[5]: http://static.zybuluo.com/yangyc/jnfldvcpbdu716oua7lltwww/%E6%88%AA%E5%B1%8F2020-03-04%E4%B8%8A%E5%8D%888.40.45.png
[6]: http://static.zybuluo.com/yangyc/kictahra8um8owk5flgng99v/%E6%88%AA%E5%B1%8F2020-03-04%E4%B8%8A%E5%8D%888.41.13.png
[7]: http://assets.processon.com/chart_image/5e5f0759e4b0f858554674c0.png
[8]: http://static.zybuluo.com/yangyc/kxq8tp8i6roe53g2olk9lzlk/%E6%88%AA%E5%B1%8F2020-02-29%E4%B8%8B%E5%8D%8812.13.07.png
[9]: http://assets.processon.com/chart_image/5e5f097ce4b099155f95f