目錄javascript
你們好,本文使用領域驅動設計的方法,從新設計最小3D程序,識別出「用戶」和「引擎」角色,給出各類設計的視圖。html
從0開發3D引擎(九):實現最小的3D程序-「繪製三角形」java
從0開發3D引擎(十一):使用領域驅動設計,從最小3D程序中提煉引擎(第二部分)git
從0開發3D引擎(補充):介紹領域驅動設計github
上文得到了下面的成果:
一、最小3D程序
二、領域驅動設計的通用語言web
Book-Demo-Triangle Github Repo數據庫
一、場景邏輯和WebGL API的調用邏輯混雜在一塊兒
二、存在重複代碼:
1)在_init函數的「初始化全部Shader」中有重複的模式
2)在_render中,渲染三個三角形的代碼很是類似
3)Utils的sendModelUniformData1和sendModelUniformData2有重複的模式
三、_init傳遞給主循環的數據過於複雜編程
咱們根據上文的成果,進行下面的設計:
一、識別最小3D程序的用戶邏輯和引擎邏輯
二、根據用戶邏輯,給出用例圖,用於設計API
三、設計分層架構,給出架構視圖
四、進行領域驅動設計的戰略設計
1)劃分引擎子域和限界上下文
2)給出限界上下文映射圖
五、進行領域驅動設計的戰術設計
1)識別領域概念
2)創建領域模型,給出領域視圖
六、設計數據,給出數據視圖
七、根據用例圖,設計分層架構的API層
八、根據API層的設計,設計分層架構的應用服務層
九、進行一些細節的設計:
1)使用Result處理錯誤
2)使用「Discriminated Union類型」來增強值對象的值類型約束
十、基本的優化canvas
持久化數據
由於咱們並無使用數據庫,不須要離線存儲,因此本文提到的持久化數據是指:從程序啓動到程序結束時,將數據保存到內存中api
//定義聚合根Scene的PO的類型 type scene = { ... }; //定義PO的類型 type po = { scene }; 「PO」的類型爲po,「Scene PO」的類型爲scene
module SceneEntity = { //定義聚合根Scene的DO的類型 type t = { ... }; }; 「Scene DO」的類型爲SceneEntity.t
這只是目前的選型,在後面的文章中咱們會修改它們。
TinyWonder
由於本系列開發的引擎的素材來自於Wonder.js,只有最小化的功能,因此叫TinyWonder
從頂層來看,包含三個部分的邏輯:建立場景、初始化、主循環
咱們依次識別它們的用戶邏輯和引擎邏輯:
一、建立場景
用戶邏輯
引擎邏輯
二、初始化
用戶邏輯
引擎邏輯
三、主循環
用戶邏輯
引擎邏輯
index.html
/* 「User.」表示這是用戶要實現的函數 「EngineJsAPI.」表示這是引擎提供的API函數 使用"xxx()"表明某個函數 */ //由用戶實現 module User = { let prepareSceneData = () => { let (canvasId, ...) = ... ... (canvasId, ...) }; ... }; let (canvasId, ...) = User.prepareSceneData(); //保存某個場景數據到引擎中 EngineJsAPI.setXXXSceneData(canvasId, ...); EngineJsAPI.進行初始化(); EngineJsAPI.開啓主循環();
初始化對應的通用語言爲:
最小3D程序的_init函數負責初始化
如今依次分析初始化的每一個步驟對應的代碼:
一、得到WebGL上下文
相關代碼爲:
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, );
用戶邏輯
咱們能夠先識別出下面的用戶邏輯:
用戶須要傳入webgl上下文的配置項到引擎中。
咱們進行相關的思考:
引擎應該增長一個傳入配置項的API嗎?
配置項應該保存到引擎中嗎?
考慮到:
因此引擎不須要增長API,也不須要保存配置項,而是在「進行初始化」的API中傳入「配置項」,使用一次後即丟棄。
引擎邏輯
二、初始化全部Shader
相關代碼爲:
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組與三角形關聯起來。
咱們進行相關的思考:
如何使GLSL組與三角形關聯?
咱們看下相關的通用語言:
三角形與Shader一一對應,而Shader又與GLSL組一一對應。
所以,咱們能夠在三角形中增長數據:Shader名稱(類型爲string),從而使三角形經過Shader名稱與GLSL組一一關聯。
更新後的三角形通用語言爲:
根據以上的分析,咱們識別出下面的用戶邏輯:
引擎邏輯
咱們如今來思考如何解決下面的不足之處:
存在重複代碼:
1)在_init函數的「初始化全部Shader」中有重複的模式
解決方案:
一、得到全部Shader的Shader名稱和GLSL組集合
二、遍歷這個集合:
1)建立Program
2)初始化Shader
這樣的話,就只須要寫一份「初始化每一個Shader」的代碼了,消除了重複。
根據以上的分析,咱們識別出下面的引擎邏輯:
三、初始化場景
相關代碼爲:
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), );
用戶邏輯
引擎邏輯
主循環對應的通用語言爲:
對應最小3D程序的_loop函數對應主循環,如今依次分析主循環的每一個步驟對應的代碼:
一、開啓主循環
相關代碼爲:
let rec _loop = data => DomExtend.requestAnimationFrame((time: float) => { _loopBody(data); _loop(data) |> ignore; });
用戶邏輯
無
引擎邏輯
如今進入_loopBody函數:
二、設置清空顏色緩衝時的顏色值
相關代碼爲:
let _clearColor = ((gl, sceneData) as data) => { WebGL1.clearColor(0., 0., 0., 1., gl); data; }; let _loopBody = data => { data |> ... |> _clearColor |> ... };
用戶邏輯
引擎邏輯
三、清空畫布
相關代碼爲:
let _clearCanvas = ((gl, sceneData) as data) => { WebGL1.clear( WebGL1.getColorBufferBit(gl) lor WebGL1.getDepthBufferBit(gl), gl, ); data; }; let _loopBody = data => { data |> ... |> _clearCanvas |> ... };
用戶邏輯
無
引擎邏輯
四、渲染
相關代碼爲:
let _loopBody = data => { data |> ... |> _render; };
用戶邏輯
無
引擎邏輯
如今進入_render函數,咱們來分析「渲染」的每一個步驟對應的代碼:
1)設置WebGL狀態
_render函數中的相關代碼爲:
WebGL1.enable(WebGL1.getDepthTest(gl), gl); WebGL1.enable(WebGL1.getCullFace(gl), gl); WebGL1.cullFace(WebGL1.getBack(gl), gl);
用戶邏輯
引擎邏輯
2)計算view matrix和projection matrix
_render函數中的相關代碼爲:
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));
用戶邏輯
無
引擎邏輯
3)計算三個三角形的model matrix
_render函數中的相關代碼爲:
let mMatrix1 = Matrix.createIdentityMatrix() |> Matrix.setTranslation(position1); let mMatrix2 = Matrix.createIdentityMatrix() |> Matrix.setTranslation(position2); let mMatrix3 = Matrix.createIdentityMatrix() |> Matrix.setTranslation(position3);
用戶邏輯
無
引擎邏輯
4)渲染第一個三角形
_render函數中的相關代碼爲:
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, );
用戶邏輯
無
引擎邏輯
2)渲染第二個和第三個三角形
_render函數中的相關代碼爲:
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, );
用戶邏輯
與「渲染第一個三角形」的用戶邏輯同樣,只是將第一個三角形的數據換成第二個和第三個三角形的數據
引擎邏輯
與「渲染第一個三角形」的引擎邏輯同樣,只是將第一個三角形的數據換成第二個和第三個三角形的數據
識別出兩個角色:
咱們把用戶邏輯中須要用戶實現的邏輯移到角色「index.html」中;
把用戶邏輯中須要調用API實現的邏輯做爲用例,移到角色「引擎」中。
獲得的用例圖以下所示:
咱們使用四層的分層架構,架構視圖以下所示:
不容許跨層訪問。
對於「API層」和「應用服務層」,咱們會在給出領域視圖後,詳細設計它們。
咱們加入了「倉庫」,使「實體」只能經過「倉庫」來操做「數據」,隔離「數據」和「實體」。
只有「實體」負責持久化數據,因此只有「實體」依賴「倉庫」,「值對象」和「領域服務」都不該該依賴「倉庫」。
之因此「倉庫」依賴了「領域服務」、「實體」、「值對象」,是由於「倉庫」須要調用它們的函數,實現「數據」的PO和領域層的DO之間的轉換。
對於「倉庫」、「數據」、PO、DO,咱們會在後面的「設計數據」中詳細分析。
「外部」負責與引擎的外部交互。
它包含兩個部分:
以下圖所示:
以下圖所示:
其中:
上下文關係的介紹詳見上下文映射圖
如今咱們來分析下防腐層(ACL)的設計,其中相關的領域模型會在後面的「領域視圖」中給出。
一、「着色器」限界上下文提供着色器的DO數據
二、「初始化全部Shader」限界上下文的領域服務BuildInitShaderData做爲防腐層,將着色器DO數據轉換爲值對象InitShader
三、「初始化全部Shader」限界上下文的領域服務InitShader遍歷值對象InitShader,初始化每一個Shader
經過這樣的設計,隔離了領域服務InitShader和「着色器」限界上下文。
根據識別的引擎邏輯,能夠得知值對象InitShader的值是全部Shader的Shader名稱和GLSL組集合,所以咱們能夠給出值對象InitShader的類型定義:
type singleInitShader = { shaderId: string, vs: string, fs: string, }; //值對象InitShader類型定義 type initShader = list(singleInitShader);
一、「場景圖」限界上下文提供場景圖的DO數據
二、「渲染」限界上下文的領域服務BuildRenderData做爲防腐層,將場景圖DO數據轉換爲值對象Render
三、「渲染」限界上下文的領域服務Render遍歷值對象Render,渲染場景中每一個三角形
經過這樣的設計,隔離了領域服務Render和「場景圖」限界上下文。
最小3D程序的_render函數的參數是渲染須要的數據,這裏稱之爲「渲染數據」。
最小3D程序的_render函數的參數以下:
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), ), ), ), ) => { ... };
如今,咱們結合識別的引擎邏輯,對渲染數據進行抽象,提煉出值對象Render,並給出值對象Render的類型定義。
由於渲染數據包含三個部分的數據:WebGL的上下文gl、場景中惟一的相機數據、場景中全部三角形的數據,因此值對象Render也應該包含這三個部分的數據:WebGL的上下文gl、相機數據、三角形數據
能夠直接把渲染數據中的WebGL的上下文gl放到值對象Render中
對於渲染數據中的「場景中惟一的相機數據」:
( ( (eyeX, eyeY, eyeZ), (centerX, centerY, centerZ), (upX, upY, upZ), ), (near, far, fovy, aspect), ),
根據識別的引擎邏輯,咱們知道在渲染場景中全部的三角形前,須要根據這些渲染數據計算一個view matrix和一個projection matrix。由於值對象Render是爲渲染全部三角形服務的,因此值對象Render的相機數據應該爲一個view matrix和一個projection matrix
對於下面的渲染數據:
(position1, position2, position3),
根據識別的引擎邏輯,咱們知道在渲染場景中全部的三角形前,須要根據這些渲染數據計算每一個三角形的model matrix,因此值對象Render的三角形數據應該包含每一個三角形的model matrix
對於下面的渲染數據:
(indices1, indices2, indices3),
根據識別的引擎邏輯,咱們知道在調用drawElements繪製每一個三角形時,須要根據這些渲染數據計算頂點個數,做爲drawElements的第二個形參,因此值對象Render的三角形數據應該包含每一個三角形的頂點個數
對於下面的渲染數據:
(program1, program2), (vertexBuffer1, indexBuffer1), (vertexBuffer2, indexBuffer2), (vertexBuffer3, indexBuffer3),
它們能夠做爲值對象Render的三角形數據。通過抽象後,值對象Render的三角形數據應該包含每一個三角形關聯的program、每一個三角形的VBO數據(一個vertex buffer和一個index buffer)
對於下面的渲染數據(三個三角形的顏色數據),咱們須要從中設計出值對象Render的三角形數據包含的顏色數據:
(color1, (color2_1, color2_2), color3),
咱們須要將其統一爲一個數據結構,才能做爲值對象Render的顏色數據。
咱們回顧下將會在本文解決的不足之處:
二、存在重複代碼:
...
2)在_render中,渲染三個三角形的代碼很是類似
3)Utils的sendModelUniformData1和sendModelUniformData2有重複的模式
這兩處的重複跟顏色的數據結構不統一是有關係的。
咱們來看下最小3D程序中相關的代碼:
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); };
經過仔細分析這些相關的代碼,咱們能夠發現這兩處的重複其實都由同一個緣由形成的:
因爲第一個和第三個三角形的顏色數據與第二個三角形的顏色數據不一樣,須要調用對應的sendModelUniformData1或sendModelUniformData2方法來傳遞對應三角形的顏色數據。
解決「Utils的sendModelUniformData1和sendModelUniformData2有重複的模式」
那是否能夠把全部三角形的顏色數據統一用一個數據結構來保存,而後在渲染三角形->傳遞三角形的顏色數據時,遍歷該數據結構,只用一個函數(而不是兩個函數:sendModelUniformData一、sendModelUniformData2)傳遞對應的顏色數據,從而解決該重複呢?
咱們來分析下三個三角形的顏色數據:
第一個和第三個三角形只有一個顏色數據,類型爲(float, float, float);
第二個三角形有兩個顏色數據,它們的類型也爲(float, float, float)。
根據分析,咱們做出下面的設計:
可使用列表來保存一個三角形全部的顏色數據,它的類型爲list((float,float,float));
在傳遞該三角形的顏色數據時,遍歷列表,傳遞每一個顏色數據。
相關僞代碼以下:
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); }); ... };
這樣咱們就解決了該重複。
解決「在_render中,渲染三個三角形的代碼很是類似」
經過「統一用一種數據結構來保存顏色數據」,就能夠構造出值對象Render,從而解決該重複了:
咱們再也不須要寫三段代碼來渲染三個三角形了,而是隻寫一段「渲染每一個三角形」的代碼,而後在遍歷值對象Render時執行它。
相關僞代碼以下:
let 渲染每一個三角形 = (每一個三角形的數據) => {...}; let _render = (...) => { ... 構造值對象Render(場景圖數據) |> 遍歷值對象Render的三角形數據((每一個三角形的數據) => { 渲染每一個三角形(每一個三角形的數據) }); ... };
經過前面對渲染數據的分析,能夠給出值對象Render的類型定義:
type triangle = { mMatrix: Js.Typed_array.Float32Array.t, vertexBuffer: WebGL1.buffer, indexBuffer: WebGL1.buffer, indexCount: int, //使用統一的數據結構 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);
識別出新的領域概念:
領域視圖以下所示,圖中包含了領域模型之間的全部聚合、組合關係,以及領域模型之間的主要依賴關係
以下圖所示:
PO Container做爲一個容器,負責保存PO到內存中。
PO Container應該爲一個全局Record,有一個可變字段po,用於保存PO
相關的設計爲:
type poContainer = { mutable po }; let poContainer = { po: 建立PO() };
這裏有兩個壞味道:
咱們應該儘可能使用局部變量和不可變數據/不可變操做,消除共享的狀態。但有時候壞味道不可避免,所以咱們使用下面的策略來處理壞味道:
咱們設計以下:
相關的設計爲:
type po = { //各個聚合根的數據 canvas, shaderManager, scene, context, vboManager };
由於如今信息不夠,因此不設計聚合根的具體數據,留到實現時再設計它們。
容器管理負責讀/寫PO Container的PO,相關設計以下:
type getPO = unit => po; type setPO = po => unit;
module Repo = { //從PO中得到ShaderManager PO,轉成ShaderManager DO,返回給領域層 type getShaderManager = unit => shaderManager; //轉換來自領域層的ShaderManager DO爲ShaderManager PO,設置到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 = { //建立各個聚合根的PO數據,如建立ShaderManager PO let create = () => { shaderManager: ..., ... }; }; module ShaderManagerRepo = { //從PO中得到ShaderManager PO的某個字段,轉成DO,返回給領域層 type getXXX = po => xxx; //轉換來自領域層的ShaderManager DO的某個字段爲ShaderManager PO的對應字段,設置到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; };
用戶爲index.html頁面,它只知道javascript,不知道Reason
咱們根據用戶的特色,決定設計原則:
首先根據用例圖的用例,劃分API模塊;
而後根據API的設計原則,在對應模塊中設計具體的API,給出API的類型簽名。
API模塊及其API的設計爲:
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); //由於「傳入一個三角形的位置數據」、「傳入一個三角形的頂點數據」、「傳入一個三角形的Shader名稱」、「傳入一個三角形的顏色數據」都屬於傳入三角形的數據,因此應該只用一個API接收三角形的這些數據,這些數據應該分紅三部分:Transform數據、Geometry數據和Material數據。API負責在場景中加入一個三角形。 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); //函數名爲「set」而不是「add」的緣由是:場景中只有一個相機,所以不須要加入操做,只須要設置惟一的相機 type setCamera = (viewMatrixData, projectionMatrixData) => unit; }; module GraphicsJsAPI = { type color4 = (float, float, float, float); type setClearColor = color4 => unit; };
咱們進行下面的設計:
目前來看,VO與DTO基本相同。
應用服務模塊及其函數設計爲:
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(這個函數的參數)與VO(Scene API的addTriangle函數的參數)有區別:VO的顏色數據類型爲array(color3),而DTO的顏色數據類型爲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; };
咱們在從0開發3D引擎(五):函數式編程及其在引擎中的應用中介紹了「使用Result來處理錯誤」,它相比「拋出異常」的錯誤處理方式,有不少優勢。
咱們在引擎中主要使用Result來處理錯誤。可是在後面的「優化」中,咱們能夠看到爲了優化,引擎也使用了「拋出異常」的錯誤處理方式。
咱們以值對象Matrix爲例,來看下如何增強值對象的值類型約束,從而在編譯檢查時確保類型正確:
Matrix的值類型爲Js.Typed_array.Float32Array.t,這樣的類型設計有個缺點:不能與其它Js.Typed_array.Float32Array.t類型的變量區分開。
所以,在Matrix中可使用Discriminated Union類型來定義「Matrix」類型:
type t = | Matrix(Js.Typed_array.Float32Array.t);
這樣就能解決該缺點了。
咱們在性能熱點處進行下面的優化:
哪些地方屬於性能熱點呢?
咱們須要進行benchmark測試來肯定性能熱點,不過通常來講下面的場景屬於性能熱點的機率比較大:
具體來講,目前引擎的適用於此處提出的優化的性能熱點爲:
let 初始化全部Shader = (...) => { ... //着色器數據中有「Discriminated Union」類型的數據,而構造後的值對象InitShader的值均爲primitive類型 構造爲值對象InitShader(着色器數據) |> //使用Result.tryCatch將異常轉換爲Result Result.tryCatch((值對象InitShader) => { //使用「拋出異常」的方式處理錯誤 根據值對象InitShader,初始化每一個Shader }); //由於值對象InitShader是隻讀數據,因此不須要將值對象InitShader更新到着色器數據中 };
let 渲染 = (...) => { ... //場景圖數據中有「Discriminated Union」類型的數據,而構造後的值對象Render的值均爲primitive類型 構造值對象Render(場景圖數據) |> //使用Result.tryCatch將異常轉換爲Result Result.tryCatch((值對象Render) => { //使用「拋出異常」的方式處理錯誤 根據值對象Render,渲染每一個三角形 }); //由於值對象Render是隻讀數據,因此不須要將值對象Render更新到場景圖數據中 };
咱們經過本文的領域驅動設計,得到了下面的成果:
一、用戶邏輯和引擎邏輯
二、分層架構視圖和每一層的設計
三、領域驅動設計的戰略成果
1)引擎子域和限界上下文劃分
2)限界上下文映射圖
四、領域驅動設計的戰術成果
1)領域概念
2)領域視圖
五、數據視圖和PO的相關設計
六、一些細節的設計
七、基本的優化
本文解決了上文的不足之處:
一、場景邏輯和WebGL API的調用邏輯混雜在一塊兒
本文識別出用戶index.html和引擎這兩個角色,分離了用戶邏輯和引擎,從而解決了這個不足
二、存在重複代碼:
1)在_init函數的「初始化全部Shader」中有重複的模式
2)在_render中,渲染三個三角形的代碼很是類似
3)Utils的sendModelUniformData1和sendModelUniformData2有重複的模式
本文提出了值對象InitShader和值對象Render,分別用一份代碼實現「初始化每一個Shader」和「渲染每一個三角形」,而後分別在遍歷對應的值對象時調用對應的一份代碼,從而消除了重複
三、_init傳遞給主循環的數據過於複雜
本文對數據進行了設計,將數據分爲VO、DTO、DO、PO,從而再也不傳遞數據,解決了這個不足
一、倉庫與領域模型之間存在循環依賴
二、沒有隔離基礎設施層的「數據」的變化對領域層的影響
如在支持多線程時,須要增長渲染線程的數據,則不該該影響支持單線程的相關代碼
三、沒有隔離「WebGL」的變化
如在支持WebGL2時,不該該影響支持WebGL1的代碼
在下文中,咱們會根據本文的成果,具體實現從最小的3D程序中提煉引擎。