從0開發3D引擎(十二):使用領域驅動設計,從最小3D程式中提煉引擎(第三部分)
阿新 • • 發佈:2020-03-05
[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