從0開發(fā)3D引擎(十):使用領(lǐng)域驅(qū)動設(shè)計,從最小3D程序中提煉引擎(第一部分)
- 上一篇博文
- 下一篇博文
- 前置知識
- 回顧上文
- 本文流程
- 解釋本文使用的領(lǐng)域驅(qū)動設(shè)計的一些概念
- 本文的領(lǐng)域驅(qū)動設(shè)計選型
- 設(shè)計
- 引擎名
- 識別最小3D程序的頂層包含的用戶邏輯和引擎邏輯
- 用偽代碼初步設(shè)計index.html
- 識別最小3D程序的初始化包含的用戶邏輯和引擎邏輯
- 識別最小3D程序的主循環(huán)包含的用戶邏輯和引擎邏輯
- 根據(jù)用戶邏輯,給出用例圖
- 設(shè)計架構(gòu),給出架構(gòu)視圖
- 劃分引擎子域和限界上下文
- 給出限界上下文映射圖
- 給出流程圖
- 識別領(lǐng)域概念
- 建立領(lǐng)域模型,給出領(lǐng)域視圖
- 設(shè)計數(shù)據(jù)
- 設(shè)計API層
- 設(shè)計應(yīng)用服務(wù)層
- 使用Result處理錯誤
- 使用“Discriminated Union類型”來加強值對象的值類型約束
- 優(yōu)化
- 總結(jié)
大家好,本文使用領(lǐng)域驅(qū)動設(shè)計的方法,重新設(shè)計最小3D程序,識別出“用戶”和“引擎”角色,給出各種設(shè)計的視圖。
上一篇博文
從0開發(fā)3D引擎(九):實現(xiàn)最小的3D程序-“繪制三角形”
下一篇博文
從0開發(fā)3D引擎(十一):使用領(lǐng)域驅(qū)動設(shè)計,從最小3D程序中提煉引擎(第二部分)
前置知識
從0開發(fā)3D引擎(補充):介紹領(lǐng)域驅(qū)動設(shè)計
回顧上文
上文獲得了下面的成果:
1、最小3D程序
2、領(lǐng)域驅(qū)動設(shè)計的通用語言
最小3D程序完整代碼地址
Book-Demo-Triangle Github Repo
通用語言

將會在本文解決的不足之處
1、場景邏輯和WebGL API的調(diào)用邏輯混雜在一起
2、存在重復(fù)代碼:
1)在_init函數(shù)的“初始化所有Shader”中有重復(fù)的模式
2)在_render中,渲染三個三角形的代碼非常相似
3)Utils的sendModelUniformData1和sendModelUniformData2有重復(fù)的模式
3、_init傳遞給主循環(huán)的數(shù)據(jù)過于復(fù)雜
本文流程
我們根據(jù)上文的成果,進(jìn)行下面的設(shè)計:
1、識別最小3D程序的用戶邏輯和引擎邏輯
2、根據(jù)用戶邏輯,給出用例圖,用于設(shè)計API
3、設(shè)計分層架構(gòu),給出架構(gòu)視圖
4、進(jìn)行領(lǐng)域驅(qū)動設(shè)計的戰(zhàn)略設(shè)計
1)劃分引擎子域和限界上下文
2)給出限界上下文映射圖
3)給出流程圖
5、進(jìn)行領(lǐng)域驅(qū)動設(shè)計的戰(zhàn)術(shù)設(shè)計
1)識別領(lǐng)域概念
2)建立領(lǐng)域模型,給出領(lǐng)域視圖
6、設(shè)計數(shù)據(jù),給出數(shù)據(jù)視圖
7、根據(jù)用例圖,設(shè)計分層架構(gòu)的API層
8、根據(jù)API層的設(shè)計,設(shè)計分層架構(gòu)的應(yīng)用服務(wù)層
9、進(jìn)行一些細(xì)節(jié)的設(shè)計:
1)使用Result處理錯誤
2)使用“Discriminated Union類型”來加強值對象的值類型約束
10、基本的優(yōu)化
解釋本文使用的領(lǐng)域驅(qū)動設(shè)計的一些概念
- 持久化數(shù)據(jù)
因為我們并沒有使用數(shù)據(jù)庫,不需要離線存儲,所以本文提到的持久化數(shù)據(jù)是指:從程序啟動到程序結(jié)束時,將數(shù)據(jù)保存到內(nèi)存中 - VO、DTO、DO、PO
這些屬于領(lǐng)域驅(qū)動設(shè)計中數(shù)據(jù)的相關(guān)概念,詳見從0開發(fā)3D引擎(補充):介紹領(lǐng)域驅(qū)動設(shè)計->數(shù)據(jù) - “PO”和“XXX PO”(XXX為聚合根名,如Scene)
“PO”是指整個PO;
“XXX PO”是指PO的XXX(聚合根)字段的PO數(shù)據(jù)。
如:
//定義聚合根Scene的PO的類型
type scene = {
...
};
//定義PO的類型
type po = {
scene
};
“PO”的類型為po,“Scene PO”的類型為scene
- “XXX DO”(XXX為聚合根名,如Scene)
“XXX DO”是指XXX(聚合根)的DO數(shù)據(jù)。
如:
module SceneEntity = {
//定義聚合根Scene的DO的類型
type t = {
...
};
};
“Scene DO”的類型為SceneEntity.t
本文的領(lǐng)域驅(qū)動設(shè)計選型
- 使用分層架構(gòu)
- 領(lǐng)域模型(領(lǐng)域服務(wù)、實體、值對象)使用貧血模型
這只是目前的選型,在后面的文章中我們會修改它們。
設(shè)計
引擎名
TinyWonder
因為本系列開發(fā)的引擎的素材來自于Wonder.js,只有最小化的功能,所以叫TinyWonder
識別最小3D程序的頂層包含的用戶邏輯和引擎邏輯
從頂層來看,包含三個部分的邏輯:創(chuàng)建場景、初始化、主循環(huán)
我們依次識別它們的用戶邏輯和引擎邏輯:
1、創(chuàng)建場景
用戶邏輯
- 準(zhǔn)備場景數(shù)據(jù)
場景數(shù)據(jù)包括canvas的id、三個三角形的數(shù)據(jù)等 - 調(diào)用API,保存某個場景數(shù)據(jù)
- 調(diào)用API,獲得某個場景數(shù)據(jù)
引擎邏輯
- 保存某個場景數(shù)據(jù)
- 獲得某個場景數(shù)據(jù)
2、初始化
用戶邏輯
- 調(diào)用API,進(jìn)行初始化
引擎邏輯
- 實現(xiàn)初始化
3、主循環(huán)
用戶邏輯
- 調(diào)用API,開啟主循環(huán)
引擎邏輯
- 實現(xiàn)主循環(huán)
用偽代碼初步設(shè)計index.html
根據(jù)對最小3D程序的頂層的分析,我們用偽代碼初步設(shè)計index.html:
index.html
/*
“User.”表示這是用戶要實現(xiàn)的函數(shù)
“EngineJsAPI.”表示這是引擎提供的API函數(shù)
使用"xxx()"代表某個函數(shù)
*/
//由用戶實現(xiàn)
module User = {
let prepareSceneData = () => {
let (canvasId, ...) = ...
...
(canvasId, ...)
};
...
};
let (canvasId, ...) = User.prepareSceneData();
//保存某個場景數(shù)據(jù)到引擎中
EngineJsAPI.setXXXSceneData(canvasId, ...);
EngineJsAPI.進(jìn)行初始化();
EngineJsAPI.開啟主循環(huán)();
識別最小3D程序的初始化包含的用戶邏輯和引擎邏輯
初始化對應(yīng)的通用語言為:

最小3D程序的_init函數(shù)負(fù)責(zé)初始化
現(xiàn)在依次分析初始化的每個步驟對應(yīng)的代碼:
1、獲得WebGL上下文
相關(guān)代碼為:
let canvas = DomExtend.querySelector(DomExtend.document, "#webgl");
let gl =
WebGL1.getWebGL1Context(
canvas,
{
"alpha": true,
"depth": true,
"stencil": false,
"antialias": true,
"premultipliedAlpha": true,
"preserveDrawingBuffer": false,
}: WebGL1.contextConfigJsObj,
);
用戶邏輯
我們可以先識別出下面的用戶邏輯:
- 準(zhǔn)備canvas的id
- 調(diào)用API,傳入canvas的id
- 準(zhǔn)備webgl上下文的配置項
用戶需要傳入webgl上下文的配置項到引擎中。
我們進(jìn)行相關(guān)的思考:
引擎應(yīng)該增加一個傳入配置項的API嗎?
配置項應(yīng)該保存到引擎中嗎?
考慮到:
- 該配置項只被使用一次,即在“獲得webgl上下文”時才需要使用配置項
- “獲得webgl上下文”是在“初始化”的時候進(jìn)行
所以引擎不需要增加API,也不需要保存配置項,而是在“進(jìn)行初始化”的API中傳入“配置項”,使用一次后即丟棄。
引擎邏輯
- 獲得canvas
- 雖然不用保存配置項,但是要根據(jù)配置項和canvas,保存從canvas獲得的webgl的上下文
2、初始化所有Shader
相關(guān)代碼為:
let program1 =
gl |> WebGL1.createProgram |> Utils.initShader(GLSL.vs1, GLSL.fs1, gl);
let program2 =
gl |> WebGL1.createProgram |> Utils.initShader(GLSL.vs2, GLSL.fs2, gl);
用戶邏輯
用戶需要將兩組GLSL傳入引擎,并且把GLSL組與三角形關(guān)聯(lián)起來。
我們進(jìn)行相關(guān)的思考:
如何使GLSL組與三角形關(guān)聯(lián)?
我們看下相關(guān)的通用語言:

三角形與Shader一一對應(yīng),而Shader又與GLSL組一一對應(yīng)。
因此,我們可以在三角形中增加數(shù)據(jù):Shader名稱(類型為string),從而使三角形通過Shader名稱與GLSL組一一關(guān)聯(lián)。
更新后的三角形通用語言為:

根據(jù)以上的分析,我們識別出下面的用戶邏輯:
- 準(zhǔn)備兩個Shader名稱
- 準(zhǔn)備兩組GLSL
- 調(diào)用API,傳入一個三角形的Shader名稱
用戶需要調(diào)用該API三次,從而把所有三角形的Shader名稱都傳入引擎 - 調(diào)用API,傳入一個Shader名稱和關(guān)聯(lián)的GLSL組
用戶需要調(diào)用該API兩次,從而把所有Shader的Shader名稱和GLSL組都傳入引擎
引擎邏輯
我們現(xiàn)在來思考如何解決下面的不足之處:
存在重復(fù)代碼:
1)在_init函數(shù)的“初始化所有Shader”中有重復(fù)的模式
解決方案:
1、獲得所有Shader的Shader名稱和GLSL組集合
2、遍歷這個集合:
1)創(chuàng)建Program
2)初始化Shader
這樣的話,就只需要寫一份“初始化每個Shader”的代碼了,消除了重復(fù)。
根據(jù)以上的分析,我們識別出下面的引擎邏輯:
- 獲得所有Shader的Shader名稱和GLSL組集合
- 遍歷這個集合
- 創(chuàng)建Program
- 初始化Shader
3、初始化場景
相關(guān)代碼為:
let (vertices1, indices1) = Utils.createTriangleVertexData();
let (vertices2, indices2) = Utils.createTriangleVertexData();
let (vertices3, indices3) = Utils.createTriangleVertexData();
let (vertexBuffer1, indexBuffer1) =
Utils.initVertexBuffers((vertices1, indices1), gl);
let (vertexBuffer2, indexBuffer2) =
Utils.initVertexBuffers((vertices2, indices2), gl);
let (vertexBuffer3, indexBuffer3) =
Utils.initVertexBuffers((vertices3, indices3), gl);
let (position1, position2, position3) = (
(0.75, 0., 0.),
((-0.), 0., 0.5),
((-0.5), 0., (-2.)),
);
let (color1, (color2_1, color2_2), color3) = (
(1., 0., 0.),
((0., 0.8, 0.), (0., 0.5, 0.)),
(0., 0., 1.),
);
let ((eyeX, eyeY, eyeZ), (centerX, centerY, centerZ), (upX, upY, upZ)) = (
(0., 0.0, 5.),
(0., 0., (-100.)),
(0., 1., 0.),
);
let (near, far, fovy, aspect) = (
1.,
100.,
30.,
(canvas##width |> Js.Int.toFloat) /. (canvas##height |> Js.Int.toFloat),
);
用戶邏輯
- 調(diào)用API,準(zhǔn)備三個三角形的頂點數(shù)據(jù)
因為每個三角形的頂點數(shù)據(jù)都一樣,所以應(yīng)該由引擎負(fù)責(zé)創(chuàng)建三角形的頂點數(shù)據(jù),然后由用戶調(diào)用三次API來準(zhǔn)備三個三角形的頂點數(shù)據(jù) - 調(diào)用API,傳入三個三角形的頂點數(shù)據(jù)
- 準(zhǔn)備三個三角形的位置數(shù)據(jù)
- 準(zhǔn)備三個三角形的顏色數(shù)據(jù)
- 準(zhǔn)備相機數(shù)據(jù)
準(zhǔn)備view matrix需要的eye、center、up向量和projection matrix需要的near、far、fovy、aspect - 調(diào)用API,傳入相機數(shù)據(jù)
引擎邏輯
- 創(chuàng)建三角形的頂點數(shù)據(jù)
- 保存三個三角形的頂點數(shù)據(jù)
- 保存三個三角形的位置數(shù)據(jù)
- 保存三個三角形的顏色數(shù)據(jù)
- 創(chuàng)建和初始化三個三角形的VBO
- 保存相機數(shù)據(jù)
保存eye、center、up向量和near、far、fovy、aspect
識別最小3D程序的主循環(huán)包含的用戶邏輯和引擎邏輯
主循環(huán)對應(yīng)的通用語言為:

對應(yīng)最小3D程序的_loop函數(shù)對應(yīng)主循環(huán),現(xiàn)在依次分析主循環(huán)的每個步驟對應(yīng)的代碼:
1、開啟主循環(huán)
相關(guān)代碼為:
let rec _loop = data =>
DomExtend.requestAnimationFrame((time: float) => {
_loopBody(data);
_loop(data) |> ignore;
});
用戶邏輯
無
引擎邏輯
- 調(diào)用requestAnimationFrame開啟主循環(huán)
現(xiàn)在進(jìn)入_loopBody函數(shù):
2、設(shè)置清空顏色緩沖時的顏色值
相關(guān)代碼為:
let _clearColor = ((gl, sceneData) as data) => {
WebGL1.clearColor(0., 0., 0., 1., gl);
data;
};
let _loopBody = data => {
data |> ... |> _clearColor |> ...
};
用戶邏輯
- 準(zhǔn)備清空顏色緩沖時的顏色值
- 調(diào)用API,傳入清空顏色緩沖時的顏色值
引擎邏輯
- 保存清空顏色緩沖時的顏色值
- 設(shè)置清空顏色緩沖時的顏色值
3、清空畫布
相關(guān)代碼為:
let _clearCanvas = ((gl, sceneData) as data) => {
WebGL1.clear(
WebGL1.getColorBufferBit(gl) lor WebGL1.getDepthBufferBit(gl),
gl,
);
data;
};
let _loopBody = data => {
data |> ... |> _clearCanvas |> ...
};
用戶邏輯
無
引擎邏輯
- 清空畫布
4、渲染
相關(guān)代碼為:
let _loopBody = data => {
data |> ... |> _render;
};
用戶邏輯
無
引擎邏輯
- 渲染
現(xiàn)在進(jìn)入_render函數(shù),我們來分析“渲染”的每個步驟對應(yīng)的代碼:
1)設(shè)置WebGL狀態(tài)
_render函數(shù)中的相關(guān)代碼為:
WebGL1.enable(WebGL1.getDepthTest(gl), gl);
WebGL1.enable(WebGL1.getCullFace(gl), gl);
WebGL1.cullFace(WebGL1.getBack(gl), gl);
用戶邏輯
- 無
引擎邏輯
- 設(shè)置WebGL狀態(tài)
2)計算view matrix和projection matrix
_render函數(shù)中的相關(guān)代碼為:
let vMatrix =
Matrix.createIdentityMatrix()
|> Matrix.setLookAt(
(eyeX, eyeY, eyeZ),
(centerX, centerY, centerZ),
(upX, upY, upZ),
);
let pMatrix =
Matrix.createIdentityMatrix()
|> Matrix.buildPerspective((fovy, aspect, near, far));
用戶邏輯
無
引擎邏輯
- 計算view matrix
- 計算projection matrix
3)計算三個三角形的model matrix
_render函數(shù)中的相關(guān)代碼為:
let mMatrix1 =
Matrix.createIdentityMatrix() |> Matrix.setTranslation(position1);
let mMatrix2 =
Matrix.createIdentityMatrix() |> Matrix.setTranslation(position2);
let mMatrix3 =
Matrix.createIdentityMatrix() |> Matrix.setTranslation(position3);
用戶邏輯
無
引擎邏輯
- 計算三個三角形的model matrix
4)渲染第一個三角形
_render函數(shù)中的相關(guān)代碼為:
WebGL1.useProgram(program1, gl);
Utils.sendAttributeData(vertexBuffer1, program1, gl);
Utils.sendCameraUniformData((vMatrix, pMatrix), program1, gl);
Utils.sendModelUniformData1((mMatrix1, color1), program1, gl);
WebGL1.bindBuffer(WebGL1.getElementArrayBuffer(gl), indexBuffer1, gl);
WebGL1.drawElements(
WebGL1.getTriangles(gl),
indices1 |> Js.Typed_array.Uint16Array.length,
WebGL1.getUnsignedShort(gl),
0,
gl,
);
用戶邏輯
無
引擎邏輯
- 根據(jù)第一個三角形的Shader名稱,獲得關(guān)聯(lián)的Program
- 渲染第一個三角形
- 使用對應(yīng)的Program
- 傳遞三角形的頂點數(shù)據(jù)
- 傳遞view matrix和projection matrix
- 傳遞三角形的model matrix
- 傳遞三角形的顏色數(shù)據(jù)
- 繪制三角形
- 根據(jù)indices計算頂點個數(shù),作為drawElements的第二個形參
2)渲染第二個和第三個三角形
_render函數(shù)中的相關(guān)代碼為:
WebGL1.useProgram(program2, gl);
Utils.sendAttributeData(vertexBuffer2, program2, gl);
Utils.sendCameraUniformData((vMatrix, pMatrix), program2, gl);
Utils.sendModelUniformData2((mMatrix2, color2_1, color2_2), program2, gl);
WebGL1.bindBuffer(WebGL1.getElementArrayBuffer(gl), indexBuffer2, gl);
WebGL1.drawElements(
WebGL1.getTriangles(gl),
indices2 |> Js.Typed_array.Uint16Array.length,
WebGL1.getUnsignedShort(gl),
0,
gl,
);
WebGL1.useProgram(program1, gl);
Utils.sendAttributeData(vertexBuffer3, program1, gl);
Utils.sendCameraUniformData((vMatrix, pMatrix), program1, gl);
Utils.sendModelUniformData1((mMatrix3, color3), program1, gl);
WebGL1.bindBuffer(WebGL1.getElementArrayBuffer(gl), indexBuffer3, gl);
WebGL1.drawElements(
WebGL1.getTriangles(gl),
indices3 |> Js.Typed_array.Uint16Array.length,
WebGL1.getUnsignedShort(gl),
0,
gl,
);
用戶邏輯
與“渲染第一個三角形”的用戶邏輯一樣,只是將第一個三角形的數(shù)據(jù)換成第二個和第三個三角形的數(shù)據(jù)
引擎邏輯
與“渲染第一個三角形”的引擎邏輯一樣,只是將第一個三角形的數(shù)據(jù)換成第二個和第三個三角形的數(shù)據(jù)
根據(jù)用戶邏輯,給出用例圖
識別出兩個角色:
- 引擎
- index.html
index.html頁面是引擎的用戶
我們把用戶邏輯中需要用戶實現(xiàn)的邏輯移到角色“index.html”中;
把用戶邏輯中需要調(diào)用API實現(xiàn)的邏輯作為用例,移到角色“引擎”中。
得到的用例圖如下所示:

設(shè)計架構(gòu),給出架構(gòu)視圖
我們使用四層的分層架構(gòu),架構(gòu)視圖如下所示:

不允許跨層訪問。
對于“API層”和“應(yīng)用服務(wù)層”,我們會在給出領(lǐng)域視圖后,詳細(xì)設(shè)計它們。
我們加入了“倉庫”,使“實體”只能通過“倉庫”來操作“數(shù)據(jù)”,隔離“數(shù)據(jù)”和“實體”。
只有“實體”負(fù)責(zé)持久化數(shù)據(jù),所以只有“實體”依賴“倉庫”,“值對象”和“領(lǐng)域服務(wù)”都不應(yīng)該依賴“倉庫”。
之所以“倉庫”依賴了“領(lǐng)域服務(wù)”、“實體”、“值對象”,是因為“倉庫”需要調(diào)用它們的函數(shù),實現(xiàn)“數(shù)據(jù)”的PO和領(lǐng)域?qū)拥腄O之間的轉(zhuǎn)換。
對于“倉庫”、“數(shù)據(jù)”,我們會在后面的“設(shè)計數(shù)據(jù)”中詳細(xì)分析。
分析“基礎(chǔ)設(shè)施層”的“外部”
“外部”負(fù)責(zé)與引擎的外部交互。
它包含兩個部分:
- Js庫
使用FFI封裝引擎調(diào)用的Js庫。 - 外部對象
使用FFI定義外部對象,如:
最小3D程序的DomExtend.re可以放在這里,因為它依賴了“window”這個外部對象;
Utils.re的error函數(shù)也可以放在這里,因為它們依賴了“js異常”這個外部對象。
劃分引擎子域和限界上下文
根據(jù)通用語言:

我們已經(jīng)劃分出了“場景圖上下文”、“初始化上下文”、“主循環(huán)上下文”,這三個限界上下文應(yīng)該分別位于三個子域中:“場景”、“初始化”、“主循環(huán)”。
現(xiàn)在我們在“初始化”子域中劃分出更多的上下文:
經(jīng)過前面識別的用戶邏輯和通過用偽代碼初步設(shè)計index.html,我們知道“初始化上下文”中的“初始化場景”步驟是由用戶實現(xiàn)的:用戶準(zhǔn)備場景數(shù)據(jù),調(diào)用引擎API設(shè)置場景數(shù)據(jù)。除了這個步驟,另外兩個步驟都由引擎實現(xiàn),因此可以將其建模為限界上下文:“保存WebGL上下文”、“初始化所有Shader”。
現(xiàn)在我們在“主循環(huán)”子域中劃分出更多的上下文:
除開事件,其它三個步驟可以建模為限界上下文:“設(shè)置清空顏色緩沖時的顏色值”、“清空畫布”、“渲染”。
現(xiàn)在我們根據(jù)通用語言和識別的引擎邏輯,劃分更多的限界上下文和子域:
根據(jù)引擎邏輯:
- 獲得canvas
- 雖然不用保存配置項,但是要根據(jù)配置項和canvas,保存從canvas獲得的webgl的上下文
可以知道限界上下文“保存WebGL上下文”需要獲得canvas,因此可以劃分限界上下文“畫布”,它對應(yīng)子域“頁面”。
根據(jù)引擎邏輯,我們知道限界上下文“初始化所有Shader”、“設(shè)置清空顏色緩沖時的顏色值”、“清空畫布”、“渲染”都需要調(diào)用WebGL上下文的方法(即調(diào)用WebGL API,如drawElements),因此可以劃分限界上下文“上下文”,它對應(yīng)子域“WebGL上下文”。
根據(jù)引擎邏輯:
- 創(chuàng)建和初始化三個三角形的VBO
- 傳遞三角形的頂點數(shù)據(jù)
需要綁定三角形的VBO的vertex buffer - 渲染三角形時要繪制三角形
需要綁定三角形的VBO的index buffer
可以知道引擎需要管理VBO,因此可以劃分限界上下文“VBO管理”,它對應(yīng)子域“WebGL對象管理”.
現(xiàn)在我們仔細(xì)分析通用語言中的“場景圖上下文”,我們可以看到該上下文實際上包含兩個聚合根:Scene和Shader。因為一個限界上下文應(yīng)該只有一個聚合根,因此這提示我們,需要劃分限界上下文“著色器”,它對應(yīng)子域“著色器”,將聚合根Shader移到該限界上下文中。
根據(jù)引擎邏輯:
- 計算view matrix
- 計算projection matrix
- 計算三個三角形的model matrix
這些邏輯需要操作矩陣和向量,因此可以劃分限界上下文“數(shù)學(xué)”,它對應(yīng)子域“數(shù)據(jù)結(jié)構(gòu)”。
另外,我需要一些通用的值對象來保存一些數(shù)據(jù),如使用值對象Color3來保存三角形的顏色數(shù)據(jù)(r、g、b三個分量)。因此可以劃分限界上下文“容器”,位于子域“數(shù)據(jù)結(jié)構(gòu)”中。
綜上所述,我們可以劃分出子域和限界上下文,如下圖所示:

給出限界上下文映射圖
現(xiàn)在我們來說明下限界上下文之間關(guān)系是怎么來的:
- 子域“數(shù)據(jù)結(jié)構(gòu)”屬于通用子域,提供給其它子域使用。它完全獨立,因此它與其它子域的關(guān)系屬于“遵奉者”
- 在前面的“劃分引擎子域和限界上下文”中,我們已經(jīng)知道限界上下文“畫布”提供數(shù)據(jù)-“一個畫布”給限界上下文“保存WebGL上下文”使用。它完全獨立,因此它們的關(guān)系屬于“遵奉者”
- 因為限界上下文“初始化所有Shader”和子域“主循環(huán)”的各個限界上下文都依賴限界上下文“保存WebGL上下文”產(chǎn)生的數(shù)據(jù):WebGL上下文,因此它們的關(guān)系屬于“客戶方——供應(yīng)方開發(fā)”
- 因為限界上下文“初始化所有Shader”需要從限界上下文“著色器”中獲得數(shù)據(jù),并作出防腐設(shè)計,因此它們的關(guān)系屬于:“開放主機服務(wù)/發(fā)布語言”->“防腐層”
- 同理,因為限界上下文“渲染”需要從限界上下文“場景圖”中獲得數(shù)據(jù),并作出防腐設(shè)計,因此它們的關(guān)系屬于:“開放主機服務(wù)/發(fā)布語言”->“防腐層”
- 因為限界上下文“渲染”依賴限界上下文“初始化所有Shader”產(chǎn)生的數(shù)據(jù):Program,所以它們的關(guān)系屬于“客戶方——供應(yīng)方開發(fā)”
- 限界上下文“VBO管理”提供給限界上下文“渲染”使用。它完全獨立,因此它們的關(guān)系屬于“遵奉者”
- 限界上下文“上下文”提供WebGL上下文的方法(即WebGL API)給子域“初始化”和子域“主循環(huán)”的各個限界上下文使用。它完全獨立,因此它們的關(guān)系屬于“遵奉者”
綜上所述,限界上下文映射圖如下圖所示:

圖中標(biāo)志的解釋:
- “U”為上游,“D”為下游
下游依賴上游 - “C”為遵奉者
- “CSD”為客戶方——供應(yīng)方開發(fā)
- “OHS”為開放主機服務(wù)
- “PL”為發(fā)布語言
- “ACL”為防腐層
DDD(領(lǐng)域驅(qū)動設(shè)計)中各種限界上下文關(guān)系的介紹詳見上下文映射圖
現(xiàn)在我們來分析下防腐層(ACL)的設(shè)計,其中相關(guān)的領(lǐng)域模型會在后面的“領(lǐng)域視圖”中給出。
“初始化所有Shader”限界上下文的防腐設(shè)計
1、“著色器”限界上下文提供著色器的DO數(shù)據(jù)
2、“初始化所有Shader”限界上下文的領(lǐng)域服務(wù)BuildInitShaderData作為防腐層,將著色器DO數(shù)據(jù)轉(zhuǎn)換為值對象InitShader
3、“初始化所有Shader”限界上下文的領(lǐng)域服務(wù)InitShader遍歷值對象InitShader,初始化每個Shader
通過這樣的設(shè)計,隔離了領(lǐng)域服務(wù)InitShader和“著色器”限界上下文。
設(shè)計值對象InitShader
根據(jù)識別的引擎邏輯,可以得知值對象InitShader的值是所有Shader的Shader名稱和GLSL組集合,因此我們可以給出值對象InitShader的類型定義:
type singleInitShader = {
shaderId: string,
vs: string,
fs: string,
};
//值對象InitShader類型定義
type initShader = list(singleInitShader);
“渲染”限界上下文的防腐設(shè)計
1、“場景圖”限界上下文提供場景圖的DO數(shù)據(jù)
2、“渲染”限界上下文的領(lǐng)域服務(wù)BuildRenderData作為防腐層,將場景圖DO數(shù)據(jù)轉(zhuǎn)換為值對象Render
3、“渲染”限界上下文的領(lǐng)域服務(wù)Render遍歷值對象Render,渲染場景中每個三角形
通過這樣的設(shè)計,隔離了領(lǐng)域服務(wù)Render和“場景圖”限界上下文。
設(shè)計值對象Render
最小3D程序的_render函數(shù)的參數(shù)是渲染需要的數(shù)據(jù),這里稱之為“渲染數(shù)據(jù)”。
最小3D程序的_render函數(shù)的參數(shù)如下:
let _render =
(
(
gl,
(
(program1, program2),
(indices1, indices2, indices3),
(vertexBuffer1, indexBuffer1),
(vertexBuffer2, indexBuffer2),
(vertexBuffer3, indexBuffer3),
(position1, position2, position3),
(color1, (color2_1, color2_2), color3),
(
(
(eyeX, eyeY, eyeZ),
(centerX, centerY, centerZ),
(upX, upY, upZ),
),
(near, far, fovy, aspect),
),
),
),
) => {
...
};
現(xiàn)在,我們結(jié)合識別的引擎邏輯,對渲染數(shù)據(jù)進(jìn)行抽象,提煉出值對象Render,并給出值對象Render的類型定義。
因為渲染數(shù)據(jù)包含三個部分的數(shù)據(jù):WebGL的上下文gl、場景中唯一的相機數(shù)據(jù)、場景中所有三角形的數(shù)據(jù),所以值對象Render也應(yīng)該包含這三個部分的數(shù)據(jù):WebGL的上下文gl、相機數(shù)據(jù)、三角形數(shù)據(jù)
可以直接把渲染數(shù)據(jù)中的WebGL的上下文gl放到值對象Render中
對于渲染數(shù)據(jù)中的“場景中唯一的相機數(shù)據(jù)”:
(
(
(eyeX, eyeY, eyeZ),
(centerX, centerY, centerZ),
(upX, upY, upZ),
),
(near, far, fovy, aspect),
),
根據(jù)識別的引擎邏輯,我們知道在渲染場景中所有的三角形前,需要根據(jù)這些渲染數(shù)據(jù)計算一個view matrix和一個projection matrix。因為值對象Render是為渲染所有三角形服務(wù)的,所以值對象Render的相機數(shù)據(jù)應(yīng)該為一個view matrix和一個projection matrix
對于下面的渲染數(shù)據(jù):
(position1, position2, position3),
根據(jù)識別的引擎邏輯,我們知道在渲染場景中所有的三角形前,需要根據(jù)這些渲染數(shù)據(jù)計算每個三角形的model matrix,所以值對象Render的三角形數(shù)據(jù)應(yīng)該包含每個三角形的model matrix
對于下面的渲染數(shù)據(jù):
(indices1, indices2, indices3),
根據(jù)識別的引擎邏輯,我們知道在調(diào)用drawElements繪制每個三角形時,需要根據(jù)這些渲染數(shù)據(jù)計算頂點個數(shù),作為drawElements的第二個形參,所以值對象Render的三角形數(shù)據(jù)應(yīng)該包含每個三角形的頂點個數(shù)
對于下面的渲染數(shù)據(jù):
(program1, program2),
(vertexBuffer1, indexBuffer1),
(vertexBuffer2, indexBuffer2),
(vertexBuffer3, indexBuffer3),
它們可以作為值對象Render的三角形數(shù)據(jù)。經(jīng)過抽象后,值對象Render的三角形數(shù)據(jù)應(yīng)該包含每個三角形關(guān)聯(lián)的program、每個三角形的VBO數(shù)據(jù)(一個vertex buffer和一個index buffer)
對于下面的渲染數(shù)據(jù)(三個三角形的顏色數(shù)據(jù)),我們需要從中設(shè)計出值對象Render的三角形數(shù)據(jù)包含的顏色數(shù)據(jù):
(color1, (color2_1, color2_2), color3),
我們需要將其統(tǒng)一為一個數(shù)據(jù)結(jié)構(gòu),才能作為值對象Render的顏色數(shù)據(jù)。
我們回顧下將會在本文解決的不足之處:
2、存在重復(fù)代碼:
...
2)在_render中,渲染三個三角形的代碼非常相似
3)Utils的sendModelUniformData1和sendModelUniformData2有重復(fù)的模式
這兩處的重復(fù)跟顏色的數(shù)據(jù)結(jié)構(gòu)不統(tǒng)一是有關(guān)系的。
我們來看下最小3D程序中相關(guān)的代碼:
Main.re
let _render =
(...) => {
...
//渲染第一個三角形
...
Utils.sendModelUniformData1((mMatrix1, color1), program1, gl);
...
//渲染第二個三角形
...
Utils.sendModelUniformData2((mMatrix2, color2_1, color2_2), program2, gl);
...
//渲染第三個三角形
...
Utils.sendModelUniformData1((mMatrix3, color3), program1, gl);
...
};
Utils.re
let sendModelUniformData1 = ((mMatrix, color), program, gl) => {
...
let colorLocation = _unsafeGetUniformLocation(program, "u_color0", gl);
...
_sendColorData(color, gl, colorLocation);
};
let sendModelUniformData2 = ((mMatrix, color1, color2), program, gl) => {
...
let color1Location = _unsafeGetUniformLocation(program, "u_color0", gl);
let color2Location = _unsafeGetUniformLocation(program, "u_color1", gl);
...
_sendColorData(color1, gl, color1Location);
_sendColorData(color2, gl, color2Location);
};
通過仔細(xì)分析這些相關(guān)的代碼,我們可以發(fā)現(xiàn)這兩處的重復(fù)其實都由同一個原因造成的:
由于第一個和第三個三角形的顏色數(shù)據(jù)與第二個三角形的顏色數(shù)據(jù)不同,需要調(diào)用對應(yīng)的sendModelUniformData1或sendModelUniformData2方法來傳遞對應(yīng)三角形的顏色數(shù)據(jù)。
解決“Utils的sendModelUniformData1和sendModelUniformData2有重復(fù)的模式”
那是否可以把所有三角形的顏色數(shù)據(jù)統(tǒng)一用一個數(shù)據(jù)結(jié)構(gòu)來保存,然后在渲染三角形->傳遞三角形的顏色數(shù)據(jù)時,遍歷該數(shù)據(jù)結(jié)構(gòu),只用一個函數(shù)(而不是兩個函數(shù):sendModelUniformData1、sendModelUniformData2)傳遞對應(yīng)的顏色數(shù)據(jù),從而解決該重復(fù)呢?
我們來分析下三個三角形的顏色數(shù)據(jù):
第一個和第三個三角形只有一個顏色數(shù)據(jù),類型為(float, float, float);
第二個三角形有兩個顏色數(shù)據(jù),它們的類型也為(float, float, float)。
根據(jù)分析,我們作出下面的設(shè)計:
可以使用列表來保存一個三角形所有的顏色數(shù)據(jù),它的類型為list((float,float,float));
在傳遞該三角形的顏色數(shù)據(jù)時,遍歷列表,傳遞每個顏色數(shù)據(jù)。
相關(guān)偽代碼如下:
let sendModelUniformData = ((mMatrix, colors: list((float,float,float))), program, gl) => {
colors
|> List.iteri((index, (r, g, b)) => {
let colorLocation =
_unsafeGetUniformLocation(program, {j|u_color$index|j}, gl);
WebGL1.uniform3f(colorLocation, r, g, b, gl);
});
...
};
這樣我們就解決了該重復(fù)。
解決“在_render中,渲染三個三角形的代碼非常相似”
通過“統(tǒng)一用一種數(shù)據(jù)結(jié)構(gòu)來保存顏色數(shù)據(jù)”,就可以構(gòu)造出值對象Render,從而解決該重復(fù)了:
我們不再需要寫三段代碼來渲染三個三角形了,而是只寫一段“渲染每個三角形”的代碼,然后在遍歷值對象Render時執(zhí)行它。
相關(guān)偽代碼如下:
let 渲染每個三角形 = (每個三角形的數(shù)據(jù)) => {...};
let _render =
(...) => {
...
構(gòu)造值對象Render(場景圖數(shù)據(jù))
|>
遍歷值對象Render的三角形數(shù)據(jù)((每個三角形的數(shù)據(jù)) => {
渲染每個三角形(每個三角形的數(shù)據(jù))
});
...
};
給出值對象Render的類型定義
通過前面對渲染數(shù)據(jù)的分析,可以給出值對象Render的類型定義:
type triangle = {
mMatrix: Js.Typed_array.Float32Array.t,
vertexBuffer: WebGL1.buffer,
indexBuffer: WebGL1.buffer,
indexCount: int,
//使用統(tǒng)一的數(shù)據(jù)結(jié)構(gòu)
colors: list((float, float, float)),
program: WebGL1.program,
};
type triangles = list(triangle);
type camera = {
vMatrix: Js.Typed_array.Float32Array.t,
pMatrix: Js.Typed_array.Float32Array.t,
};
type gl = WebGL1.webgl1Context;
//值對象Render類型定義
type render = (gl, camera, triangles);
給出流程圖
根據(jù)前面的“給出限界上下文映射圖”中的上下文之間的關(guān)系,我們可以決定子域“初始化”和子域“主循環(huán)”的各個限界上下文之間的執(zhí)行順序:
子域“初始化”的流程圖如下所示:

子域“主循環(huán)”的流程圖如下所示:

識別領(lǐng)域概念
識別出新的領(lǐng)域概念:
- Transform
我們識別出“Transform”的概念,用它來在坐標(biāo)系中定位三角形。
Transform的數(shù)據(jù)包括三角形的位置、旋轉(zhuǎn)和縮放。在當(dāng)前場景中,Transform數(shù)據(jù) = 三角形的位置 - Geometry
我們識別出“Geometry”的概念,用它來表達(dá)三角形的形狀。
Geometry的數(shù)據(jù)包括三角形的頂點數(shù)據(jù)和VBO。在當(dāng)前場景中,Geometry數(shù)據(jù) = 三角形的Vertices、Indices和對應(yīng)的VBO - Material
我們識別出“Material”的概念,用它來表達(dá)三角形的材質(zhì)。
Material的數(shù)據(jù)包括三角形的著色器、顏色、紋理、光照。在當(dāng)前場景中,Material數(shù)據(jù) = 三角形的Shader名稱 + 三角形的顏色
建立領(lǐng)域模型,給出領(lǐng)域視圖
領(lǐng)域視圖如下所示,圖中包含了領(lǐng)域模型之間的所有聚合、組合關(guān)系,以及領(lǐng)域模型之間的主要依賴關(guān)系

設(shè)計數(shù)據(jù)
分層數(shù)據(jù)視圖
如下圖所示:

設(shè)計PO Container
PO Container作為一個容器,負(fù)責(zé)保存PO到內(nèi)存中。
PO Container應(yīng)該為一個全局Record,有一個可變字段po,用于保存PO
相關(guān)的設(shè)計為:
type poContainer = {
mutable po
};
let poContainer = {
po: 創(chuàng)建PO()
};
這里有兩個壞味道:
- poContainer為全局變量
這是為了讓poContainer在程序啟動到終止期間,一直存在于內(nèi)存中 - 使用了可變字段po
這是為了在設(shè)置PO到poContainer中時,讓poContainer在內(nèi)存中始終只有一份
我們應(yīng)該盡量使用局部變量和不可變數(shù)據(jù)/不可變操作,消除共享的狀態(tài)。但有時候壞味道不可避免,因此我們使用下面的策略來處理壞味道:
- 把壞味道集中和隔離到一個可控的范圍
- 使用容器來封裝副作用
如函數(shù)內(nèi)部發(fā)生錯誤時,可以用容器來包裝錯誤信息,返回給函數(shù)外部,在外部的某處(可控的范圍)集中處理錯誤。詳見后面的“使用Result處理錯誤”
設(shè)計PO
我們設(shè)計如下:
- 用Record作為PO的數(shù)據(jù)結(jié)構(gòu)
- PO的字段對應(yīng)聚合根的數(shù)據(jù)
- PO是不可變數(shù)據(jù)
相關(guān)的設(shè)計為:
type po = {
//各個聚合根的數(shù)據(jù)
canvas,
shaderManager,
scene,
context,
vboManager
};
因為現(xiàn)在信息不夠,所以不設(shè)計聚合根的具體數(shù)據(jù),留到實現(xiàn)時再設(shè)計它們。
設(shè)計容器管理
容器管理負(fù)責(zé)讀/寫PO Container的PO,相關(guān)設(shè)計如下:
type getPO = unit => po;
type setPO = po => unit;
設(shè)計倉庫
職責(zé)
- 將來自領(lǐng)域?qū)拥腄O轉(zhuǎn)換為PO,設(shè)置到PO Container中
- 從PO Container中獲得PO,轉(zhuǎn)換為DO傳遞給領(lǐng)域?qū)?/li>
偽代碼和類型簽名
module Repo = {
//從PO中獲得ShaderManager PO,轉(zhuǎn)成ShaderManager DO,返回給領(lǐng)域?qū)? type getShaderManager = unit => shaderManager;
//轉(zhuǎn)換來自領(lǐng)域?qū)拥腟haderManager DO為ShaderManager PO,設(shè)置到PO中
type setShaderManager = shaderManager => unit;
type getCanvas = unit => canvas;
type setCanvas = canvas => unit;
type getScene = unit => scene;
type setScene = scene => unit;
type getVBOManager = unit => vboManager;
type setVBOManager = vboManager => unit;
type getContext = unit => context;
type setContext = context => unit;
};
module CreateRepo = {
//創(chuàng)建各個聚合根的PO數(shù)據(jù),如創(chuàng)建ShaderManager PO
let create = () => {
shaderManager: ...,
...
};
};
module ShaderManagerRepo = {
//從PO中獲得ShaderManager PO的某個字段,轉(zhuǎn)成DO,返回給領(lǐng)域?qū)? type getXXX = po => xxx;
//轉(zhuǎn)換來自領(lǐng)域?qū)拥腟haderManager DO的某個字段為ShaderManager PO的對應(yīng)字段,設(shè)置到PO中
type setXXX = (...) => unit;
};
module CanvasRepo = {
type getXXX = unit => xxx;
type setXXX = (...) => unit;
};
module SceneRepo = {
type getXXX = unit => xxx;
type setXXX = (...) => unit;
};
module VBOManagerRepo = {
type getXXX = unit => xxx;
type setXXX = (...) => unit;
};
module ContextRepo = {
type getXXX = unit => xxx;
type setXXX = (...) => unit;
};
設(shè)計API層
職責(zé)
- 將index.html輸入的VO轉(zhuǎn)換為DTO,傳遞給應(yīng)用服務(wù)層
- 將應(yīng)用服務(wù)層輸出的DTO轉(zhuǎn)換為VO,返回給用戶index.html
API層的用戶的特點
用戶為index.html頁面,它只知道javascript,不知道Reason
引擎API的設(shè)計原則
我們根據(jù)用戶的特點,決定設(shè)計原則:
- 應(yīng)該對用戶隱藏API層下面的層級
如:
用戶不應(yīng)該知道基礎(chǔ)設(shè)施層的“數(shù)據(jù)”的存在。 - 應(yīng)該對用戶隱藏實現(xiàn)的細(xì)節(jié)
如:
用戶需要一個API來獲得canvas,而引擎API通過“非純”操作來獲得canvas并返回給用戶。
用戶不需要知道是怎樣獲得canvas的,所以API的名稱應(yīng)該為getCanvas,而不應(yīng)該為unsafeGetCanvas(在引擎中,如果我們通過“非純”操作獲得了某個值,則稱該操作為unsafe) - 輸入和輸出應(yīng)該為VO,而VO的類型為javascript的數(shù)據(jù)類型
- 應(yīng)該對用戶隱藏Reason語言的語法
如:
不應(yīng)該對用戶暴露Reason語言的Record等數(shù)據(jù)結(jié)構(gòu),但可以對用戶暴露Reason語言的Tuple,因為它與javascript的數(shù)組類型相同 - 應(yīng)該對用戶隱藏Reason語言的類型
如:
API的輸入?yún)?shù)和輸出結(jié)果應(yīng)該為javascript的數(shù)據(jù)類型,不能為Reason獨有的類型
(
Reason的string,int等類型與javascript的數(shù)據(jù)類型相同,可以作為API的輸入?yún)?shù)和輸出結(jié)果;
但是Reason的Discriminated Union類型、抽象類型等類型是Reason獨有的,不能作為API的輸入?yún)?shù)和輸出結(jié)果。
)
- 應(yīng)該對用戶隱藏Reason語言的語法
劃分API模塊,設(shè)計具體的API
首先根據(jù)用例圖的用例,劃分API模塊;
然后根據(jù)API的設(shè)計原則,在對應(yīng)模塊中設(shè)計具體的API,給出API的類型簽名。
API模塊及其API的設(shè)計為:
module DirectorJsAPI = {
//WebGL1.contextConfigJsObj是webgl上下文配置項的類型
type init = WebGL1.contextConfigJsObj => unit;
type start = unit => unit;
};
module CanvasJsAPI = {
type canvasId = string;
type setCanvasById = canvasId => unit;
};
module ShaderJsAPI = {
type shaderName = string;
type vs = string;
type fs = string;
type addGLSL = (shaderName, (vs, fs)) => unit;
};
module SceneJsAPI = {
type vertices = Js.Typed_array.Float32Array.t;
type indices = Js.Typed_array.Uint16Array.t;
type createTriangleVertexData = unit => (vertices, indices);
//因為“傳入一個三角形的位置數(shù)據(jù)”、“傳入一個三角形的頂點數(shù)據(jù)”、“傳入一個三角形的Shader名稱”、“傳入一個三角形的顏色數(shù)據(jù)”都屬于傳入三角形的數(shù)據(jù),所以應(yīng)該只用一個API接收三角形的這些數(shù)據(jù),這些數(shù)據(jù)應(yīng)該分成三部分:Transform數(shù)據(jù)、Geometry數(shù)據(jù)和Material數(shù)據(jù)。API負(fù)責(zé)在場景中加入一個三角形。
type position = (float, float, float);
type vertices = Js.Typed_array.Float32Array.t;
type indices = Js.Typed_array.Uint16Array.t;
type shaderName = string;
type color3 = (float, float, float);
type addTriangle =
(position, (vertices, indices), (shaderName, array(color3))) => unit;
type eye = (float, float, float);
type center = (float, float, float);
type up = (float, float, float);
type viewMatrixData = (eye, center, up);
type near = float;
type far = float;
type fovy = float;
type aspect = float;
type projectionMatrixData = (near, far, fovy, aspect);
//函數(shù)名為“set”而不是“add”的原因是:場景中只有一個相機,因此不需要加入操作,只需要設(shè)置唯一的相機
type setCamera = (viewMatrixData, projectionMatrixData) => unit;
};
module GraphicsJsAPI = {
type color4 = (float, float, float, float);
type setClearColor = color4 => unit;
};
設(shè)計應(yīng)用服務(wù)層
職責(zé)
- 將API層輸入的DTO轉(zhuǎn)換為DO,傳遞給領(lǐng)域?qū)?/li>
- 將領(lǐng)域?qū)虞敵龅腄O轉(zhuǎn)換為DTO,返回給API層
- 處理錯誤
設(shè)計應(yīng)用服務(wù)
我們進(jìn)行下面的設(shè)計:
- API層模塊與應(yīng)用服務(wù)層的應(yīng)用服務(wù)模塊一一對應(yīng)
- API與應(yīng)用服務(wù)的函數(shù)一一對應(yīng)
目前來看,VO與DTO基本相同。
應(yīng)用服務(wù)模塊及其函數(shù)設(shè)計為:
module DirectorApService = {
type init = WebGL1.contextConfigJsObj => unit;
type start = unit => unit;
};
module CanvasApService = {
type canvasId = string;
type setCanvasById = canvasId => unit;
};
module ShaderApService = {
type shaderName = string;
type vs = string;
type fs = string;
type addGLSL = (shaderName, (vs, fs)) => unit;
};
module SceneApService = {
type vertices = Js.Typed_array.Float32Array.t;
type indices = Js.Typed_array.Uint16Array.t;
type createTriangleVertexData = unit => (vertices, indices);
type position = (float, float, float);
type vertices = Js.Typed_array.Float32Array.t;
type indices = Js.Typed_array.Uint16Array.t;
type shaderName = string;
type color3 = (float, float, float);
//注意:DTO(這個函數(shù)的參數(shù))與VO(Scene API的addTriangle函數(shù)的參數(shù))有區(qū)別:VO的顏色數(shù)據(jù)類型為array(color3),而DTO的顏色數(shù)據(jù)類型為list(color3)
type addTriangle =
(position, (vertices, indices), (shaderName, list(color3))) => unit;
type eye = (float, float, float);
type center = (float, float, float);
type up = (float, float, float);
type viewMatrixData = (eye, center, up);
type near = float;
type far = float;
type fovy = float;
type aspect = float;
type projectionMatrixData = (near, far, fovy, aspect);
type setCamera = (viewMatrixData, projectionMatrixData) => unit;
};
module GraphicsApService = {
type color4 = (float, float, float, float);
type setClearColor = color4 => unit;
};
使用Result處理錯誤
我們在從0開發(fā)3D引擎(五):函數(shù)式編程及其在引擎中的應(yīng)用中介紹了“使用Result來處理錯誤”,它相比“拋出異常”的錯誤處理方式,有很多優(yōu)點。
我們在引擎中主要使用Result來處理錯誤。但是在后面的“優(yōu)化”中,我們可以看到為了優(yōu)化,引擎也使用了“拋出異常”的錯誤處理方式。
使用“Discriminated Union類型”來加強值對象的值類型約束
我們以值對象Matrix為例,來看下如何加強值對象的值類型約束,從而在編譯檢查時確保類型正確:
Matrix的值類型為Js.Typed_array.Float32Array.t,這樣的類型設(shè)計有個缺點:不能與其它Js.Typed_array.Float32Array.t類型的變量區(qū)分開。
因此,在Matrix中可以使用Discriminated Union類型來定義“Matrix”類型:
type t =
| Matrix(Js.Typed_array.Float32Array.t);
這樣就能解決該缺點了。
優(yōu)化
我們在性能熱點處進(jìn)行下面的優(yōu)化:
- 處理錯誤優(yōu)化
因為使用“拋出異常”的方式處理錯誤不需要操作容器Result,性能更好,所以在性能熱點處:
使用“拋出異常”的方式處理錯誤,然后在上一層使用Result.tryCatch將異常轉(zhuǎn)換為Result
在其它地方:
直接用Result包裝錯誤信息 - Discriminated Union類型優(yōu)化
因為操作“Discriminated Union類型”需要操作容器,性能較差,所以在性能熱點處:
1、在性能熱點開始前,通過一次遍歷操作,將所有相關(guān)的值對象的值從“Discriminated Union類型”中取出來。其中取出的值是primitive類型,即int、string等沒有用容器包裹的原始類型
2、在性能熱點處操作primtive類型的值
3、在性能熱點結(jié)束后,通過一次遍歷操作,將更新后的primitive類型的值寫到“Discriminated Union類型”中
哪些地方屬于性能熱點呢?
我們需要進(jìn)行benchmark測試來確定性能熱點,不過一般來說下面的場景屬于性能熱點的概率比較大:
- 遍歷數(shù)量大的集合
如遍歷場景中所有的三角形,因為通常場景有至少上千個模型。 - 雖然遍歷數(shù)量小的集合,但每次遍歷的時間或內(nèi)存開銷大
如遍歷場景中所有的Shader,因為通常場景有只幾十個到幾百個Shader,數(shù)量不是很多,但是在每次遍歷時會初始化Shader,造成較大的時間開銷。
具體來說,目前引擎的適用于此處提出的優(yōu)化的性能熱點為:
- 初始化所有Shader時,優(yōu)化“遍歷和初始化每個Shader”
優(yōu)化的偽代碼為:
let 初始化所有Shader = (...) => {
...
//著色器數(shù)據(jù)中有“Discriminated Union”類型的數(shù)據(jù),而構(gòu)造后的值對象InitShader的值均為primitive類型
構(gòu)造為值對象InitShader(著色器數(shù)據(jù))
|>
//使用Result.tryCatch將異常轉(zhuǎn)換為Result
Result.tryCatch((值對象InitShader) => {
//使用“拋出異常”的方式處理錯誤
根據(jù)值對象InitShader,初始化每個Shader
});
//因為值對象InitShader是只讀數(shù)據(jù),所以不需要將值對象InitShader更新到著色器數(shù)據(jù)中
};
- 渲染時,優(yōu)化“遍歷和渲染每個三角形”
優(yōu)化的偽代碼為:
let 渲染 = (...) => {
...
//場景圖數(shù)據(jù)中有“Discriminated Union”類型的數(shù)據(jù),而構(gòu)造后的值對象Render的值均為primitive類型
構(gòu)造值對象Render(場景圖數(shù)據(jù))
|>
//使用Result.tryCatch將異常轉(zhuǎn)換為Result
Result.tryCatch((值對象Render) => {
//使用“拋出異常”的方式處理錯誤
根據(jù)值對象Render,渲染每個三角形
});
//因為值對象Render是只讀數(shù)據(jù),所以不需要將值對象Render更新到場景圖數(shù)據(jù)中
};
總結(jié)
本文成果
我們通過本文的領(lǐng)域驅(qū)動設(shè)計,獲得了下面的成果:
1、用戶邏輯和引擎邏輯
2、分層架構(gòu)視圖和每一層的設(shè)計
3、領(lǐng)域驅(qū)動設(shè)計的戰(zhàn)略成果
1)引擎子域和限界上下文劃分
2)限界上下文映射圖
3)流程圖
4、領(lǐng)域驅(qū)動設(shè)計的戰(zhàn)術(shù)成果
1)領(lǐng)域概念
2)領(lǐng)域視圖
5、數(shù)據(jù)視圖和PO的相關(guān)設(shè)計
6、一些細(xì)節(jié)的設(shè)計
7、基本的優(yōu)化
本文解決了上文的不足之處:
1、場景邏輯和WebGL API的調(diào)用邏輯混雜在一起
本文識別出用戶index.html和引擎這兩個角色,分離了用戶邏輯和引擎,從而解決了這個不足
2、存在重復(fù)代碼:
1)在_init函數(shù)的“初始化所有Shader”中有重復(fù)的模式
2)在_render中,渲染三個三角形的代碼非常相似
3)Utils的sendModelUniformData1和sendModelUniformData2有重復(fù)的模式
本文提出了值對象InitShader和值對象Render,分別只用一份代碼實現(xiàn)“初始化每個Shader”和“渲染每個三角形”,然后分別在遍歷對應(yīng)的值對象時調(diào)用對應(yīng)的一份代碼,從而消除了重復(fù)
3、_init傳遞給主循環(huán)的數(shù)據(jù)過于復(fù)雜
本文對數(shù)據(jù)進(jìn)行了設(shè)計,將數(shù)據(jù)分為VO、DTO、DO、PO,從而不再傳遞數(shù)據(jù),解決了這個不足
本文不足之處
1、倉庫與領(lǐng)域模型之間存在循環(huán)依賴
2、沒有隔離基礎(chǔ)設(shè)施層的“數(shù)據(jù)”的變化對領(lǐng)域?qū)拥挠绊?br>
如在支持多線程時,需要增加渲染線程的數(shù)據(jù),則不應(yīng)該影響支持單線程的相關(guān)代碼
3、沒有隔離“WebGL”的變化
如在支持WebGL2時,不應(yīng)該影響支持WebGL1的代碼
下文概要
在第二部分-第四部分中,我們會根據(jù)本文的成果,具體實現(xiàn)從最小的3D程序中提煉引擎。
浙公網(wǎng)安備 33010602011771號