從0開發3D引擎(十一):使用領域驅動設計,從最小3D程式中提煉引擎(第二部分)

大家好,本文根據領域驅動設計的成果,開始實現從最小的3D程式中提煉引擎。

上一篇博文

從0開發3D引擎(十):使用領域驅動設計,從最小3D程式中提煉引擎(第一部分)

本文流程

我們根據上文的成果,按照下面的步驟開始實現從最小的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命令:
yarn start

2、在瀏覽器地址中輸入下面的url並回車,即可運行index.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

這是按照分層架構來劃分的文件夾:

  • 一級文件夾(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

引擎子域文件夾的子文件夾對應該子域的限界上下文,如下圖所示:
截屏2020-03-04上午8.34.32.png-32.2kB

限界上下文文件夾的子文件均為entity/、value_object/、service/,分別放置實體、值對象和領域服務的模組文件。
部分截圖如下圖所示:
截屏2020-03-04上午8.37.22.png-22.3kB

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

ContainerManager.re負責實現「容器管理」

container/放置PO Container相關的模組文件

po/放置PO類型定義的文件

infrastructure_layer/external/文件夾結構如下圖所示:
截屏2020-03-04上午8.41.13.png-5.6kB

external_object/放置外部對象的FFI文件
library/放置js庫的FFI文件

index.html實現調用引擎API

index.html需要引入引擎文件,調用它的API。

我們首先考慮的實現方案是:
與最小3D程式一樣,index.html以ES6 module的方式引入要使用的引擎的每個模組文件(一個.re的引擎文件就是一個模組文件),調用暴露的API函數。
index.html的相關偽程式碼如下:

<script type="module">  import { setCanvasById } from "./lib/es6_global/src/api_layer/api/CameraJsAPI.js";  import { start } from "./lib/es6_global/src/api_layer/api/DirectorJsAPI.js";    window.onload = () => {      ...      setCanvasById(canvasId);      ...      start();      ...  };  </script>

這個方案有下面的缺點:

  • 用戶訪問的許可權過大
    用戶可以訪問非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,它的程式碼如下:

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

舉個例子來說明用戶如何使用generate函數:
假設用戶工作在TinyWonder項目(項目根目錄為/y/Github/TinyWonder/)上,創建了./src/api/AJsAPI.re,它的程式碼為:

let aFunc = v => 1;

它的模組名為「AJsAPI」。

用戶還創建了./src/api/ddd/BJsAPI.re,它的程式碼為:

let bFunc = v => v * 2;

它的模組名為「BJsAPI」

用戶可以在TinyWonder項目根目錄上,用js程式碼調用Tiny-Wonder-GenerateIndex項目的generate函數:

//將路徑為「/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文件,它的程式碼如下:

module A = {    let aFunc = AJsAPI.aFunc;  };    module B = {    let bFunc = BJsAPI.bFunc;  };

現在我們清楚了如何使用generate函數,那麼繼續在TinyWonderGenerateIndex項目上完成剩餘工作:
c)編譯該項目的Reason程式碼為Commonjs模組規範的js文件
d)通過npm發布該項目
package.json需要定義main欄位:

"main": "./lib/js/src/Generate.js",

e)回到TinyWonder項目
f)在TinyWonder項目的gulpfile.js->generateIndex任務中,引入TinyWonderGenerateIndex項目,調用它的generate函數
TinyWonder項目的相關程式碼為:
gulpfile.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文件:

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:

"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函數:

let aFunc = v => Js.log(v);    let bFunc = v => Js.log(v * 2);

2)安裝gulp後,在項目根目錄上執行generateIndex任務,進行運行測試:

gulp generateIndex

可以看到src/中加入了Index.re文件,Index.re程式碼為:

module Test = {    let aFunc = TestJsAPI.aFunc;      let bFunc = TestJsAPI.bFunc;  };

3)在安裝webpack後,在項目根目錄上執行下面命令,打包為dist/wd.js文件,其中命名空間為「wd」:

yarn webpack

4)index.html引入wd.js文件,調用引擎API

index.html的程式碼為:

  <script src="./dist/wd.js"></script>      <script>      var a = wd.Test.aFunc(1);      var b = wd.Test.bFunc(2);    </script>

5)在瀏覽器上運行index.html,打開控制台,可以看到列印了"1"和「4」

用偽程式碼實現index.html

我們根據用例圖和設計的API來實現index.html:
index.html需要使用最小3D程式的數據,來實現用例圖中「index.html」角色包含的用戶邏輯;
index.html需要調用引擎API,來實現用例圖中的用例。

index.html的偽程式碼實現如下所示:

<canvas id="webgl" width="400" height="400">    Please use a browser that supports "canvas"  </canvas>    <script>    //準備canvas的id    var canvasId = "webgl";      CanvasJsAPI.setCanvasById(canvasId);        //準備清空顏色緩衝時的顏色值    var clearColor = [0.0, 0.0, 0.0, 1.0];      GraphicsJsAPI.setClearColor(clearColor);        //準備兩組GLSL    var vs1 = `  precision mediump float;  attribute vec3 a_position;  uniform mat4 u_pMatrix;  uniform mat4 u_vMatrix;  uniform mat4 u_mMatrix;    void main() {  gl_Position = u_pMatrix * u_vMatrix * u_mMatrix * vec4(a_position, 1.0);  }  `;    var fs1 = `  precision mediump float;    uniform vec3 u_color0;    void main(){  gl_FragColor = vec4(u_color0, 1.0);  }  `;    var vs2 = `  precision mediump float;  attribute vec3 a_position;  uniform mat4 u_pMatrix;  uniform mat4 u_vMatrix;  uniform mat4 u_mMatrix;    void main() {  gl_Position = u_pMatrix * u_vMatrix * u_mMatrix * vec4(a_position, 1.0);  }  `;    var fs2 = `  precision mediump float;    uniform vec3 u_color0;  uniform vec3 u_color1;    void main(){  gl_FragColor = vec4(u_color0 * u_color1, 1.0);  }  `;    //準備兩個Shader的名稱    var shaderName1 = "shader1";    var shaderName2 = "shader2";      ShaderJsAPI.addGLSL(shaderName1, [vs1, fs1]);    ShaderJsAPI.addGLSL(shaderName2, [vs2, fs2]);        //調用API,準備三個三角形的頂點數據    var [vertices1, indices1] = SceneJsAPI.createTriangleVertexData();    var [vertices2, indices2] = SceneJsAPI.createTriangleVertexData();    var [vertices3, indices3] = SceneJsAPI.createTriangleVertexData();    //準備三個三角形的位置數據    var [position1, position2, position3] = [      [0.75, 0.0, 0.0],      [-0.0, 0.0, 0.5],      [-0.5, 0.0, -2.0]    ];    //準備三個三角形的顏色數據    var [colors1, colors2, colors3] = [      [[1.0, 0.0, 0.0]],      [[0.0, 0.8, 0.0], [0.0, 0.5, 0.0]],      [[0.0, 0.0, 1.0]]    ];      SceneJsAPI.addTriangle(position1, [vertices1, indices1], [shaderName1, colors1]);    SceneJsAPI.addTriangle(position2, [vertices2, indices2], [shaderName2, colors2]);    SceneJsAPI.addTriangle(position3, [vertices3, indices3], [shaderName1, colors3]);        //準備相機數據    var [eye, center, up] = [      [0.0, 0.0, 5.0],      [0.0, 0.0, -100.0],      [0.0, 1.0, 0.0]    ];    var [near, far, fovy, aspect] = [      1.0,      100.0,      30.0,      canvas.width / canvas.height    ];      SceneJsAPI.setCamera([eye, center, up], [near, far, fovy, aspect]);        //準備webgl上下文的配置項    var contextConfig = {      "alpha": true,      "depth": true,      "stencil": false,      "antialias": true,      "premultipliedAlpha": true,      "preserveDrawingBuffer": false,    };      DirectorJsAPI.init(contextConfig);        DirectorJsAPI.start();  </script>

我們按照下面的步驟來具體實現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程式碼為:

//因為應用服務CanvasApService的setCanvasById函數輸入和輸出的DTO與這裡(API)的setCanvasById函數輸入和輸出的VO相同,所以不需要在兩者之間轉換  let setCanvasById = CanvasApService.setCanvasById;

2、在src/application_layer/service/中加入CanvasApService.re,實現應用服務
CanvasApService.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程式碼為:

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程式碼為:

//這裡定義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程式碼為:

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程式碼為:

//因為在創建PO時,Canvas PO(一個畫布)並不存在,所以它為option類型  type po = {canvas: option(CanvasPOType.canvas)};

7、在src/infrastructure_layer/data/po/中加入CanvasPOType.re,定義Canvas PO類型
CanvasPOType.re程式碼為:

type canvas = DomExtend.htmlElement;

8、因為定義了option類型,所以加入領域服務Option
1)在領域視圖的「容器」限界上下文中,加入領域服務Option,負責操作Option容器
2)在src/domain_layer/domain/structure/container/service/中加入OptionContainerDoService.re,創建領域服務Option
OptionContainerDoService.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程式碼為:

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程式碼為:

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程式碼為:

type poContainer = {mutable po: POType.po};

Container.re程式碼為:

let poContainer: ContainerType.poContainer = {    po: CreateRepo.create(),  };

12、在src/domain_layer/repo/中加入CreateRepo.re,實現創建PO
CreateRepo.re程式碼為:

open POType;    let create = () => {canvas: None};

13、在項目根目錄上執行webpack命令,更新wd.js文件

yarn webpack

14、實現index.html相關程式碼

index.html程式碼為:

<!DOCTYPE html>  <html lang="en">    <head>    <meta charset="utf-8" />    <title>use engine</title>  </head>    <body>    <canvas id="webgl" width="400" height="400">      Please use a browser that supports "canvas"    </canvas>      <script src="./dist/wd.js"></script>      <script>      //準備canvas的id      var canvasId = "webgl";        //調用API      wd.Canvas.setCanvasById(canvasId);    </script>  </body>    </html>

15、運行測試

運行index.html頁面

打開控制台,可以看到列印了"webgl"

實現「GraphicsJsAPI.setClearColor」

1、在src/api_layer/api/中加入GraphicsJsAPI.re,實現API
GraphicsJsAPI.re程式碼為:

let setClearColor = GraphicsApService.setClearColor;

2、在src/application_layer/service/中加入GraphicsApService.re,實現應用服務
GraphicsApService.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程式碼為:

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程式碼為:

let setClearColor = clearColor => {    ContextRepo.setClearColor(clearColor);  };

5、在src/infrastructure_layer/data/po/中加入ContextPOType.re,定義Context PO類型
ContextPOType.re程式碼為:

type context = {clearColor: (float, float, float, float)};

6、修改POType.re
POType.re相關程式碼為:

type po = {    ...    context: ContextPOType.context,  };

7、在src/domain_layer/repo/中加入ContextRepo.re,實現倉庫對Context PO的clearColor欄位的操作
ContextRepo.re程式碼為:

let getClearColor = () => {    Repo.getContext().clearColor;  };    let setClearColor = clearColor => {    Repo.setContext({      ...Repo.getContext(),      clearColor: Color4ContainerVO.value(clearColor),    });  };

8、修改Repo.re,實現倉庫對Context PO的操作
Repo.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相關程式碼為:

let create = () => {    ...    context: {      clearColor: (0., 0., 0., 1.),    },  };

10、在項目根目錄上執行webpack命令,更新wd.js文件

yarn webpack

11、實現index.html相關程式碼

index.html程式碼為:

  <script>      ...      //準備清空顏色緩衝時的顏色值      var clearColor = [0.0, 0.0, 0.0, 1.0];        wd.Graphics.setClearColor(clearColor);    </script>

12、運行測試

運行index.html頁面

打開控制台,可以看到列印了數組:[0,0,0,1]

實現「ShaderJsAPI.addGLSL」

1、在src/api_layer/api/中加入ShaderJsAPI.re,實現API
ShaderJsAPI.re程式碼為:

let addGLSL = ShaderApService.addGLSL;

2、設計領域模型ShaderManager、Shader、GLSL的DO

根據領域模型:
此處輸入圖片的描述
和識別的引擎邏輯:
獲得所有Shader的Shader名稱和GLSL組集合

我們可以設計聚合根ShaderManager的DO為集合list:

type t = {glsls: list(Shader DO)};

設計值對象GLSL的DO為:

type t =    | GLSL(string, string);

設計實體Shader的DO為:

type shaderName = string;    type t =    | Shader(shaderName, GLSL DO);

3、在src/application_layer/service/中加入ShaderApService.re,實現應用服務
ShaderApService.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程式碼為:

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程式碼為:

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程式碼為:

type t = {glsls: list(ShaderShaderEntity.t)};    let addGLSL = shader => {    ShaderManagerRepo.addGLSL(shader);  };

7、在src/infrastructure_layer/data/po/中加入ShaderManagerPOType.re,定義ShaderManager PO的類型
ShaderManagerPOType.re程式碼為:

//shaderId就是Shader的名稱  type shaderId = string;    type shaderManager = {glsls: list((shaderId, (string, string)))};

8、修改POType.re
POType.re相關程式碼為:

type po = {    ...    shaderManager: ShaderManagerPOType.shaderManager,  };

9、在src/domain_layer/repo/中加入ShaderManagerRepo.re,實現倉庫對ShaderManager PO的glsls欄位的操作
ShaderManagerRepo.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相關程式碼為:

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相關程式碼為:

let create = () => {    ...    shaderManager: {      glsls: [],    },  };

12、在項目根目錄上執行webpack命令,更新wd.js文件

yarn webpack

13、實現index.html相關程式碼

index.html程式碼為:

  <script>      ...      //準備兩組GLSL      var vs1 = `  precision mediump float;  attribute vec3 a_position;  uniform mat4 u_pMatrix;  uniform mat4 u_vMatrix;  uniform mat4 u_mMatrix;    void main() {  gl_Position = u_pMatrix * u_vMatrix * u_mMatrix * vec4(a_position, 1.0);  }  `;      var fs1 = `  precision mediump float;    uniform vec3 u_color0;    void main(){  gl_FragColor = vec4(u_color0, 1.0);  }  `;      var vs2 = `  precision mediump float;  attribute vec3 a_position;  uniform mat4 u_pMatrix;  uniform mat4 u_vMatrix;  uniform mat4 u_mMatrix;    void main() {  gl_Position = u_pMatrix * u_vMatrix * u_mMatrix * vec4(a_position, 1.0);  }  `;      var fs2 = `  precision mediump float;    uniform vec3 u_color0;  uniform vec3 u_color1;    void main(){  gl_FragColor = vec4(u_color0 * u_color1, 1.0);  }  `;      //準備兩個Shader的名稱      var shaderName1 = "shader1";      var shaderName2 = "shader2";        wd.Shader.addGLSL(shaderName1, [vs1, fs1]);      wd.Shader.addGLSL(shaderName2, [vs2, fs2]);    </script>

14、運行測試

運行index.html頁面

打開控制台,可以看到列印了兩個Shader的數據:
截屏2020-02-29下午12.13.07.png-78.1kB

實現「SceneJsAPI.createTriangleVertexData」

1、在src/api_layer/api/中加入SceneJsAPI.re,實現API
SceneJsAPI.re程式碼為:

let createTriangleVertexData = SceneApService.createTriangleVertexData;

2、在src/application_layer/service/中加入SceneApService.re,實現應用服務
SceneApService.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程式碼為:

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程式碼為:

open Js.Typed_array;    type t =    | Vertices(Float32Array.t);    let create = value => Vertices(value);    let value = vertices =>    switch (vertices) {    | Vertices(value) => value    };

IndicesSceneGraphVO.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文件

yarn webpack

6、實現index.html相關程式碼

index.html程式碼為:

  <script>      ...      //調用API,準備三個三角形的頂點數據      var [vertices1, indices1] = wd.Scene.createTriangleVertexData();      var [vertices2, indices2] = wd.Scene.createTriangleVertexData();      var [vertices3, indices3] = wd.Scene.createTriangleVertexData();    </script>

7、運行測試

運行index.html頁面

打開控制台,可以看到列印了三次頂點數據

實現「SceneJsAPI.addTriangle」

1、修改SceneJsAPI.re,實現API
SceneJsAPI.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

根據領域模型:
此處輸入圖片的描述

我們按照Scene的聚合關係,從下往上開始設計:
設計值對象Vector的DO為:

type t =    | Vector(float, float, float);

設計值對象Position的DO為:

type t =    | Position(Vector.t);

設計值對象Vertices的DO為:

open Js.Typed_array;    type t =    | Vertices(Float32Array.t);

設計值對象Indices的DO為:

open Js.Typed_array;    type t =    | Indices(Uint16Array.t);

設計值對象Color3的DO為:

type r = float;  type g = float;  type b = float;    type t =    | Color3(r, g, b);

設計值對象Transform的DO為:

type t = {position: Position DO};

設計值對象Geometry的DO為:

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程式碼為:

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程式碼為:

type t = {glsls: list((ShaderShaderEntity.t, GLSLShaderVO.t))};    let addGLSL = (shader, glsl) => {    ShaderManagerRepo.addGLSL(shader, glsl);  };

3)重寫ShaderApService.re
ShaderApService.re程式碼為:

let addGLSL = (shaderName, glsl) => {    ShaderManagerShaderEntity.addGLSL(      ShaderShaderEntity.create(shaderName),      GLSLShaderVO.create(glsl),    );      //用於運行測試    Js.log((shaderName, glsl));  };

4)重寫ShaderManagerRepo.re
ShaderManagerRepo.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為:

type t = {    shader: Shader DO,    colors: list(Color3 DO),  };

注意:這裡的欄位名是「shader」而不是「shaderName」或者「shaderId」,因為這樣才能直接體現Material組合了一個Shader,而不是組合了一個Shader名稱或Shader id

我們繼續設計,設計值對象Triangle的DO為:

type t = {    transform: Transform DO,    geometry: Geometry DO,    material: Material DO,  };

設計聚合根Scene的DO為:

type t = {triangles: list(Triangle DO)};

3、修改SceneApService.re,實現應用服務
SceneApService.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程式碼為:

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程式碼為:

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程式碼為:

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程式碼為:

type t = {position: PositionSceneGraphVO.t};

5)修改GeometrySceneGraphVO.re,定義DO
GeometrySceneGraphVO.re相關程式碼為:

type t = {    vertices: VerticesSceneGraphVO.t,    indices: IndicesSceneGraphVO.t,  };

6)在src/domain_layer/domain/scene/scene_graph/value_object/中加入MaterialSceneGraphVO.re,創建值對象Material
MaterialSceneGraphVO.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程式碼為:

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程式碼為:

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程式碼為:

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相關程式碼為:

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程式碼為:

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程式碼為:

open ScenePOType;    let create = (transform, geometry, material) => {    transform,    geometry,    material,  };

4)在src/domain_layer/repo/scene/中加入TransformSceneRepo.re,實現創建Scene PO的一個Triangle的Transform數據
TransformSceneRepo.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程式碼為:

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程式碼為:

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相關程式碼為:

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相關程式碼為:

let create = () => {    ...    scene: {      triangles: [],    },  };

11、在項目根目錄上執行webpack命令,更新wd.js文件

yarn webpack

12、實現index.html相關程式碼

index.html程式碼為:

  <script>      ...      //準備三個三角形的位置數據      var [position1, position2, position3] = [        [0.75, 0.0, 0.0],        [-0.0, 0.0, 0.5],        [-0.5, 0.0, -2.0]      ];      //準備三個三角形的顏色數據      var [colors1, colors2, colors3] = [        [[1.0, 0.0, 0.0]],        [[0.0, 0.8, 0.0], [0.0, 0.5, 0.0]],        [[0.0, 0.0, 1.0]]      ];        wd.Scene.addTriangle(position1, [vertices1, indices1], [shaderName1, colors1]);      wd.Scene.addTriangle(position2, [vertices2, indices2], [shaderName2, colors2]);      wd.Scene.addTriangle(position3, [vertices3, indices3], [shaderName1, colors3]);    </script>

13、運行測試

運行index.html頁面

打開控制台,可以看到列印了三次Scene PO的數據

實現「SceneJsAPI.setCamera」

1、修改SceneJsAPI.re,實現API
SceneJsAPI.re相關程式碼為:

let setCamera = SceneApService.setCamera;

2、修改SceneApService.re,實現應用服務
SceneApService.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程式碼為:

type t =    | Eye(VectorMathVO.t);    let create = value => Eye(value);    let value = eye =>    switch (eye) {    | Eye(value) => value    };

CenterSceneGraphVO.re程式碼為:

type t =    | Center(VectorMathVO.t);    let create = value => Center(value);    let value = center =>    switch (center) {    | Center(value) => value    };

UpSceneGraphVO.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程式碼為:

type t =    | Near(float);    let create = value => Near(value);    let value = near =>    switch (near) {    | Near(value) => value    };

FarSceneGraphVO.re程式碼為:

type t =    | Far(float);    let create = value => Far(value);    let value = far =>    switch (far) {    | Far(value) => value    };

FovySceneGraphVO.re程式碼為:

type t =    | Fovy(float);    let create = value => Fovy(value);    let value = fovy =>    switch (fovy) {    | Fovy(value) => value    };

AspectSceneGraphVO.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程式碼為:

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相關程式碼為:

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相關程式碼為:

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程式碼為:

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相關程式碼為:

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相關程式碼為:

let create = () => {    ...    scene: {      ...      camera: None,    },  };

9、在項目根目錄上執行webpack命令,更新wd.js文件

yarn webpack

10、實現index.html相關程式碼

index.html程式碼為:

  <script>      ...      //準備相機數據      var [eye, center, up] = [        [0.0, 0.0, 5.0],        [0.0, 0.0, -100.0],        [0.0, 1.0, 0.0]      ];      var canvas = document.querySelector("#webgl");      var [near, far, fovy, aspect] = [        1.0,        100.0,        30.0,        canvas.width / canvas.height      ];        wd.Scene.setCamera([eye, center, up], [near, far, fovy, aspect]);    </script>

11、運行測試

運行index.html頁面

打開控制台,可以看到列印了一次Scene PO的數據,它包含Camera的數據