[TOC] 大家好,本文根據領域驅動設計的成果,實現了init API。 # 上一篇博文 [從0開發3D引擎(十一):使用領域驅動設計,從最小3D程式中提煉引擎(第二部分)](https://www.cnblogs.com/chaogex/p/12411575.html) # 下一篇博文 [從0開發3D引擎(十三):使用領域驅動設計,從最小3D程式中提煉引擎(第四部分)](https://www.cnblogs.com/chaogex/p/12418292.html) # 繼續實現 ## 實現“DirectorJsAPI.init” ### 實現“儲存WebGL上下文”限界上下文 1、在src/api_layer/api/中加入DirectorJsAPI.re,實現API DirectorJsAPI.re程式碼為: ```re let init = DirectorApService.init; ``` 2、在src/infrastructure_layer/external/external_object/中加入Error.re,負責處理“js異常”這個外部物件 Error.re程式碼為: ```re //根據錯誤資訊(string型別),建立並丟擲“js異常”物件 let error = msg => Js.Exn.raiseError(msg); //根據“js異常”物件,丟擲它 let throwError: Js.Exn.t => unit = [%raw err => {| throw err; |}]; ``` 3、在src/application_layer/service/中加入DirectorApService.re,實現應用服務 DirectorApService.re程式碼為: ```re let init = contextConfigJsObj => { CanvasCanvasEntity.getCanvas() |> OptionContainerDoService.get //OptionContainerDoService.get函式返回的是Result容器的包裝值,需要呼叫ResultContainerVO.bind函式來處理容器內部的值 |> ResultContainerVO.bind(canvas => { SetWebGLContextSetWebGLContextDoService.setGl( contextConfigJsObj, canvas, ) }) //應用服務DirectorApService負責用丟擲異常的方式處理Result錯誤 |> ResultContainerVO.handleFail(Error.throwError); }; ``` 關於bind函式的使用,可以參考[從0開發3D引擎(五):函數語言程式設計及其在引擎中的應用->bind](https://www.cnblogs.com/chaogex/p/12172930.html#bind) 4、修改CanvasCanvasEntity.re,實現getCanvas函式 CanvasCanvasEntity.re相關程式碼為: ```re let getCanvas = () => { Repo.getCanvas(); }; ``` 5、把最小3D程式的WebGL1.re放到src/infrastructure_layer/external/library/中,保留所有的程式碼 WebGL1.re程式碼為: ```re open Js.Typed_array; type webgl1Context; type program; type shader; type buffer; type attributeLocation = int; type uniformLocation; type bufferTarget = | ArrayBuffer | ElementArrayBuffer; type usage = | Static; type contextConfigJsObj = { . "alpha": bool, "depth": bool, "stencil": bool, "antialias": bool, "premultipliedAlpha": bool, "preserveDrawingBuffer": bool, }; [@bs.send] external getWebGL1Context: ('canvas, [@bs.as "webgl"] _, contextConfigJsObj) => webgl1Context = "getContext"; [@bs.send.pipe: webgl1Context] external createProgram: program = ""; [@bs.send.pipe: webgl1Context] external useProgram: program => unit = ""; [@bs.send.pipe: webgl1Context] external linkProgram: program => unit = ""; [@bs.send.pipe: webgl1Context] external shaderSource: (shader, string) => unit = ""; [@bs.send.pipe: webgl1Context] external compileShader: shader => unit = ""; [@bs.send.pipe: webgl1Context] external createShader: int => shader = ""; [@bs.get] external getVertexShader: webgl1Context => int = "VERTEX_SHADER"; [@bs.get] external getFragmentShader: webgl1Context => int = "FRAGMENT_SHADER"; [@bs.get] external getHighFloat: webgl1Context => int = "HIGH_FLOAT"; [@bs.get] external getMediumFloat: webgl1Context => int = "MEDIUM_FLOAT"; [@bs.send.pipe: webgl1Context] external getShaderParameter: (shader, int) => bool = ""; [@bs.get] external getCompileStatus: webgl1Context => int = "COMPILE_STATUS"; [@bs.get] external getLinkStatus: webgl1Context => int = "LINK_STATUS"; [@bs.send.pipe: webgl1Context] external getProgramParameter: (program, int) => bool = ""; [@bs.send.pipe: webgl1Context] external getShaderInfoLog: shader => string = ""; [@bs.send.pipe: webgl1Context] external getProgramInfoLog: program => string = ""; [@bs.send.pipe: webgl1Context] external attachShader: (program, shader) => unit = ""; [@bs.send.pipe: webgl1Context] external bindAttribLocation: (program, int, string) => unit = ""; [@bs.send.pipe: webgl1Context] external deleteShader: shader => unit = ""; [@bs.send.pipe: webgl1Context] external createBuffer: buffer = ""; [@bs.get] external getArrayBuffer: webgl1Context => bufferTarget = "ARRAY_BUFFER"; [@bs.get] external getElementArrayBuffer: webgl1Context => bufferTarget = "ELEMENT_ARRAY_BUFFER"; [@bs.send.pipe: webgl1Context] external bindBuffer: (bufferTarget, buffer) => unit = ""; [@bs.send.pipe: webgl1Context] external bufferFloat32Data: (bufferTarget, Float32Array.t, usage) => unit = "bufferData"; [@bs.send.pipe: webgl1Context] external bufferUint16Data: (bufferTarget, Uint16Array.t, usage) => unit = "bufferData"; [@bs.get] external getStaticDraw: webgl1Context => usage = "STATIC_DRAW"; [@bs.send.pipe: webgl1Context] external getAttribLocation: (program, string) => attributeLocation = ""; [@bs.send.pipe: webgl1Context] external getUniformLocation: (program, string) => Js.Null.t(uniformLocation) = ""; [@bs.send.pipe: webgl1Context] external vertexAttribPointer: (attributeLocation, int, int, bool, int, int) => unit = ""; [@bs.send.pipe: webgl1Context] external enableVertexAttribArray: attributeLocation => unit = ""; ""; [@bs.send.pipe: webgl1Context] external uniformMatrix4fv: (uniformLocation, bool, Float32Array.t) => unit = ""; [@bs.send.pipe: webgl1Context] external uniform1i: (uniformLocation, int) => unit = ""; [@bs.send.pipe: webgl1Context] external uniform3f: (uniformLocation, float, float, float) => unit = ""; [@bs.send.pipe: webgl1Context] external drawElements: (int, int, int, int) => unit = ""; [@bs.get] external getFloat: webgl1Context => int = "FLOAT"; [@bs.send.pipe: webgl1Context] external clearColor: (float, float, float, float) => unit = ""; [@bs.send.pipe: webgl1Context] external clear: int => unit = ""; [@bs.get] external getColorBufferBit: webgl1Context => int = "COLOR_BUFFER_BIT"; [@bs.get] external getDepthBufferBit: webgl1Context => int = "DEPTH_BUFFER_BIT"; [@bs.get] external getDepthTest: webgl1Context => int = "DEPTH_TEST"; [@bs.send.pipe: webgl1Context] external enable: int => unit = ""; [@bs.get] external getTriangles: webgl1Context => int = "TRIANGLES"; [@bs.get] external getUnsignedShort: webgl1Context => int = "UNSIGNED_SHORT"; [@bs.get] external getCullFace: webgl1Context => int = "CULL_FACE"; [@bs.send.pipe: webgl1Context] external cullFace: int => unit = ""; [@bs.get] external getBack: webgl1Context => int = "BACK"; ``` 6、在src/domain_layer/domain/init/set_webgl_context/service/中加入SetWebGLContextSetWebGLContextDoService.re,建立領域服務SetWebGLContext SetWebGLContextSetWebGLContextDoService.re程式碼為: ```re let setGl = (contextConfigJsObj, canvas): ResultContainerVO.t(unit, Js.Exn.t) => { ContextContextEntity.setGl(contextConfigJsObj, canvas) |> ResultContainerVO.succeed; }; ``` 7、修改ContextContextEntity.re,實現setGl函式 ContextContextEntity.re相關程式碼為: ```re let setGl = (contextConfigJsObj, canvas) => { ContextRepo.setGl(WebGL1.getWebGL1Context(canvas, contextConfigJsObj)); }; ``` 8、修改ContextPOType.re,定義Context PO的gl欄位的資料型別 ContextPOType.re相關程式碼為: ```re type context = { gl: option(WebGL1.webgl1Context), ... }; ``` 9、修改ContextRepo.re,實現倉庫對Context PO的gl欄位的操作 ContextRepo.re程式碼為: ```re let getGl = gl => { //將Option轉換為Result Repo.getContext().gl |> OptionContainerDoService.get; }; let setGl = gl => { Repo.setContext({...Repo.getContext(), gl: Some(gl)}); }; ``` 10、修改CreateRepo.re,實現建立Context PO的gl欄位 CreateRepo.re相關程式碼為: ```re let create = () => { ... context: { gl: None, ... }, }; ``` ### 實現“初始化所有Shader”限界上下文 1、重寫DirectorApService.re DirectorApService.re程式碼為: ```re let init = contextConfigJsObj => { CanvasCanvasEntity.getCanvas() |> ResultContainerVO.bind(canvas => { SetWebGLContextSetWebGLContextDoService.setGl( contextConfigJsObj, canvas, ) |> ResultContainerVO.bind(() => {InitShaderInitShaderDoService.init()}) }) |> ResultContainerVO.handleFail(Error.throwError); }; ``` 2、加入值物件InitShader 在[從0開發3D引擎(十):使用領域驅動設計,從最小3D程式中提煉引擎(第一部分)](https://www.cnblogs.com/chaogex/p/12408831.html)的“設計值物件InitShader”中,我們已經定義了值物件InitShader的型別,所以我們直接將設計轉換為實現: 在src/domain_layer/domain/init/init_shader/value_object/中加入InitShaderInitShaderVO.re,建立值物件InitShader InitShaderInitShaderVO.re程式碼為: ```re type singleInitShader = { shaderId: string, vs: string, fs: string, }; type t = list(singleInitShader); ``` 3、在src/domain_layer/domain/shader/shader/value_object/中加入ProgramShaderVO.re,建立值物件Program,它的DO對應一個WebGL的program物件 ProgramShaderVO.re程式碼為: ```re type t = | Program(WebGL1.program); let create = program => Program(program); let value = program => switch (program) { | Program(value) => value }; ``` 4、修改聚合根ShaderManager的DO 根據識別的引擎邏輯: - 在初始化所有Shader時,建立每個Program - 在渲染每個三角形時,根據Shader名稱獲得關聯的Program 我們需要根據Shader id獲得關聯的Program,所以在ShaderManager DO中應該加入一個immutable hash map,它的key為Shader id,value為值物件Program的DO。 應該在領域檢視的“容器”限界上下文中,加入值物件ImmutableHashMap、值物件MutableHashMap,其中ImmutableHashMap用於實現不可變的hash map,MutableHashMap用於實現可變的hash map。 現在來具體實現它們: 1)在src/domain_layer/domain/structure/container/value_object/中建立資料夾hash_map/ 2)在hash_map/資料夾中加入ImmutableHashMapContainerVO.re、MutableHashMapContainerVO.re、HashMapContainer.re、HashMapContainerType.re ImmutableHashMapContainerVO.re負責實現Immutable Hash Map; MutableHashMapContainerVO.re負責實現Mutable Hash Map; HashMapContainer.re從兩者中提出的公共程式碼; HashMapContainerType.re定義HashMap的型別。 因為HashMapContainer需要使用reduce來遍歷陣列,這個操作屬於通用操作,應該作為領域服務,所以在領域檢視的“容器”限界上下文中,加入領域服務Array。在src/domain_layer/domain/structure/container/service/中加入ArrayContainerDoService.re,建立領域服務Array。 相關程式碼如下: ArrayContainterDoService.re ```re let reduceOneParam = (func, param, arr) => { //此處為了優化,使用for迴圈和mutable變數來代替Array.reduce let mutableParam = ref(param); for (i in 0 to Js.Array.length(arr) - 1) { mutableParam := func(. mutableParam^, Array.unsafe_get(arr, i)); }; mutableParam^; }; ``` HashMapContainerType.re ```re type t('key, 'value) = Js.Dict.t('value); type t2('value) = t(string, 'value); ``` HashMapContainer.re ```re let createEmpty = (): HashMapContainerType.t2('a) => Js.Dict.empty(); let get = (key: string, map: HashMapContainerType.t2('a)) => Js.Dict.get(map, key); let entries = (map: HashMapContainerType.t2('a)): array((Js.Dict.key, 'a)) => map |> Js.Dict.entries; let _mutableSet = (key: string, value, map) => { Js.Dict.set(map, key, value); map; }; let _createEmpty = (): Js.Dict.t('a) => Js.Dict.empty(); let copy = (map: HashMapContainerType.t2('a)): HashMapContainerType.t2('a) => map |> entries |> ArrayContainerDoService.reduceOneParam( (. newMap, (key, value)) => newMap |> _mutableSet(key, value), _createEmpty(), ); ``` ImmutableHashMapContainerVO.re ```re type t('key, 'value) = HashMapContainerType.t('key, 'value); let createEmpty = HashMapContainer.createEmpty; let set = (key: string, value: 'a, map: HashMapContainerType.t2('a)) : HashMapContainerType.t2('a) => { let newMap = map |> HashMapContainer.copy; Js.Dict.set(newMap, key, value); newMap; }; let get = HashMapContainer.get; ``` MutableHashMap.re ```re type t('key, 'value) = HashMapContainerType.t('key, 'value); let createEmpty = HashMapContainer.createEmpty; let set = (key: string, value: 'a, map: HashMapContainerType.t2('a)) => { Js.Dict.set(map, key, value); map; }; let get = HashMapContainer.get; ``` 現在我們可以通過修改ShaderManagerShaderEntity.re來修改ShaderManager的DO,加入programMap欄位 ShaderManagerShaderEntity.re相關程式碼為: ```re type t = { ... programMap: ImmutableHashMapContainerVO.t2(ShaderShaderEntity.t, ProgramShaderVO.t), }; ``` 5、建立領域服務BuildInitShaderData,實現構造值物件InitShader 1)在src/domain_layer/domain/init/init_shader/service/中加入BuildInitShaderDataInitShaderDoService.re,建立領域服務BuildInitShaderData BuildInitShaderDataInitShaderDoService.re程式碼為: ```re let build = () => { ShaderManagerShaderEntity.getAllGLSL() |> List.map(((shaderName, glsl)) => { ( { shaderId: ShaderShaderEntity.getId(shaderName), vs: GLSLShaderVO.getVS(glsl), fs: GLSLShaderVO.getFS(glsl), }: InitShaderInitShaderVO.singleInitShader ) }); }; ``` 2)修改GLSLShaderVO.re,實現getVS、getFS函式 GLSLShaderVO.re相關程式碼為: ```re let getVS = glsl => switch (glsl) { | GLSL(vs, fs) => vs }; let getFS = glsl => switch (glsl) { | GLSL(vs, fs) => fs }; ``` 3)修改ShaderManagerShaderEntity.re,加入getAllGLSL函式 ShaderManagerShaderEntity.re相關程式碼為: ```re let getAllGLSL = () => { ShaderManagerRepo.getAllGLSL(); }; ``` 4)修改ShaderManagerRepo.re,加入getAllGLSL函式 ShaderManagerShaderEntity.re相關程式碼為: ```re let getAllGLSL = () => { Repo.getShaderManager().glsls |> List.map(((shaderId, (vs, fs))) => { (ShaderShaderEntity.create(shaderId), GLSLShaderVO.create((vs, fs))) }); }; ``` 6、在src/domain_layer/domain/init/init_shader/service/中加入InitShaderInitShaderDoService.re,建立領域服務InitShader InitShaderInitShaderDoService.re程式碼為: ```re let init = (): ResultContainerVO.t(unit, Js.Exn.t) => { ContextContextEntity.getGl() |> ResultContainerVO.bind(gl => { //從著色器DO資料中構建值物件InitShader BuildInitShaderDataInitShaderDoService.build() |> ResultContainerVO.tryCatch(initShaderData => { initShaderData |> List.iter( ( {shaderId, vs, fs}: InitShaderInitShaderVO.singleInitShader, ) => { let program = ContextContextEntity.createProgram(gl); /* 注意:領域服務不應該直接依賴Repo 應該通過實體ContextContextEntity而不是ShaderManagerRepo來將program設定到ShaderManager PO的programMap中! */ ContextContextEntity.setProgram(shaderId, program); ContextContextEntity.initShader(vs, fs, program, gl) |> ignore; //用於執行測試 Js.log((shaderId, vs, fs)); }) }) }); }; ``` 7、修改ContextContextEntity.re,實現相關函式 ContextContextEntity.re相關程式碼為: ```re let getGl = () => { ContextRepo.getGl(); }; ... let createProgram = gl => gl |> WebGL1.createProgram; let setProgram = (shaderId, program) => { ShaderManagerRepo.setProgram(shaderId, program); }; let _compileShader = (gl, glslSource, shader) => { WebGL1.shaderSource(shader, glslSource, gl); WebGL1.compileShader(shader, gl); WebGL1.getShaderParameter(shader, WebGL1.getCompileStatus(gl), gl) === false ? { let message = WebGL1.getShaderInfoLog(shader, gl); //這裡為了實現“從0開發3D引擎(十):使用領域驅動設計,從最小3D程式中提煉引擎(第一部分)”提出的“處理錯誤優化”,用“丟擲異常”而不是Result來處理錯誤 Error.error( {j|shader info log: $message glsl source: $glslSource |j}, ); } : shader; }; let _linkProgram = (program, gl) => { WebGL1.linkProgram(program, gl); WebGL1.getProgramParameter(program, WebGL1.getLinkStatus(gl), gl) === false ? { let message = WebGL1.getProgramInfoLog(program, gl); //這裡為了實現“從0開發3D引擎(十):使用領域驅動設計,從最小3D程式中提煉引擎(第一部分)”提出的“處理錯誤優化”,用“丟擲異常”而不是Result來處理錯誤 Error.error({j|link program error: $message|j}); } : program; }; let initShader = (vsSource: string, fsSource: string, program, gl) => { let vs = _compileShader( gl, vsSource, WebGL1.createShader(WebGL1.getVertexShader(gl), gl), ); let fs = _compileShader( gl, fsSource, WebGL1.createShader(WebGL1.getFragmentShader(gl), gl), ); WebGL1.attachShader(program, vs, gl); WebGL1.attachShader(program, fs, gl); WebGL1.bindAttribLocation(program, 0, "a_position", gl); _linkProgram(program, gl); WebGL1.deleteShader(vs, gl); WebGL1.deleteShader(fs, gl); program; }; ``` 8、修改ShaderManagerPOType.re,ShaderManager PO加入programMap欄位 雖然programMap也是hash map,但不能直接使用領域層的值物件ImmutableHashMapContainerVO來定義它的型別!因為PO屬於基礎設施層,它不能依賴領域層! 因此,我們應該在基礎設施層的“資料”中建立一個ImmutableHashMap.re模組,儘管它的型別和函式都與ImmutableHashMapContainerVO一樣。 在src/infrastructure_layer/data/中建立資料夾structure/,在該資料夾中加入ImmutableHashMap.re。 為了方便,目前暫時直接用ImmutableHashMapContainerVO來實現ImmutableHashMap。 ImmutableHashMap.re程式碼為: ```re type t2('key, 'a) = ImmutableHashMapContainerVO.t2('key, 'a); let createEmpty = ImmutableHashMapContainerVO.createEmpty; let set = ImmutableHashMapContainerVO.set; ``` 修改ShaderManagerPOType.re,ShaderManager PO加入programMap欄位: ```re type shaderManager = { ... programMap: ImmutableHashMap.t2(shaderId, WebGL1.program), }; ``` 9、修改ShaderManagerRepo.re,實現setProgram函式 ShaderManagerRepo.re相關程式碼為: ```re let _getProgramMap = ({programMap}) => programMap; let setProgram = (shaderId, program) => { Repo.setShaderManager({ ...Repo.getShaderManager(), programMap: _getProgramMap(Repo.getShaderManager()) //這裡也使用基礎設施層的“資料”的ImmutableHashMap,因為操作的是ShaderManager PO的programMap |> ImmutableHashMap.set(shaderId, program), }); }; ``` 10、修改CreateRepo.re,實現建立ShaderManager PO的programMap欄位 CreateRepo.re相關程式碼為: ```re let create = () => { ... shaderManager: { ... programMap: ImmutableHashMap.createEmpty(), }, }; ``` ### 實現使用者程式碼並執行測試 1、在專案根目錄上執行webpack命令,更新wd.js檔案 ```js yarn webpack ``` 2、實現index.html相關程式碼 index.html程式碼為: ```html ``` 3、執行測試 執行index.html頁面 開啟控制檯,可以看到列印了兩次陣列,每次陣列內容為[Shader名稱, vs, fs],其中第一次的Shader名稱為“shader2”,第二次為“sha