[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 Please use a browser that supports "canvas" ``` 我們按照下面的步驟來具體實現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 Please use a browser that supports "canvas" ``` 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