從0開發3D引擎(十):使用領域驅動設計,從最小3D程序中提煉引擎(第一部分)

你們好,本文使用領域驅動設計的方法,從新設計最小3D程序,識別出「用戶」和「引擎」角色,給出各類設計的視圖。html

上一篇博文

從0開發3D引擎(九):實現最小的3D程序-「繪製三角形」java

下一篇博文

從0開發3D引擎(十一):使用領域驅動設計,從最小3D程序中提煉引擎(第二部分)git

前置知識

從0開發3D引擎(補充):介紹領域驅動設計github

回顧上文

上文得到了下面的成果:
一、最小3D程序
二、領域驅動設計的通用語言web

最小3D程序完整代碼地址

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

  • 「PO」和「XXX PO」(XXX爲聚合根名,如Scene)
    「PO」是指整個PO;
    「XXX PO」是指PO的XXX(聚合根)字段的PO數據。
    如:
//定義聚合根Scene的PO的類型
type scene = {
    ...
};

//定義PO的類型
type po = {
    scene
};

「PO」的類型爲po,「Scene PO」的類型爲scene
  • 「XXX DO」(XXX爲聚合根名,如Scene)
    「XXX DO」是指XXX(聚合根)的DO數據。
    如:
module SceneEntity = {
    //定義聚合根Scene的DO的類型
    type t = {
        ...
    };
};

「Scene DO」的類型爲SceneEntity.t

本文的領域驅動設計選型

  • 使用分層架構
  • 領域模型(領域服務、實體、值對象)使用貧血模型

這只是目前的選型,在後面的文章中咱們會修改它們。

設計

引擎名

TinyWonder

由於本系列開發的引擎的素材來自於Wonder.js,只有最小化的功能,因此叫TinyWonder

識別最小3D程序的頂層包含的用戶邏輯和引擎邏輯

從頂層來看,包含三個部分的邏輯:建立場景、初始化、主循環

咱們依次識別它們的用戶邏輯和引擎邏輯:
一、建立場景
用戶邏輯

  • 準備場景數據
    場景數據包括canvas的id、三個三角形的數據等
  • 調用API,保存某個場景數據
  • 調用API,得到某個場景數據

引擎邏輯

  • 保存某個場景數據
  • 得到某個場景數據

二、初始化

用戶邏輯

  • 調用API,進行初始化

引擎邏輯

  • 實現初始化

三、主循環

用戶邏輯

  • 調用API,開啓主循環

引擎邏輯

  • 實現主循環

根據對最小3D程序的頂層的分析,用僞代碼初步設計index.html

index.html

/*
「User.」表示這是用戶要實現的函數
「EngineJsAPI.」表示這是引擎提供的API函數

使用"xxx()"表明某個函數
*/

//由用戶實現
module User = {
    let prepareSceneData = () => {
        let (canvasId, ...) = ...
        
        ...
        
        (canvasId, ...)
    };
    
    ...
};

let (canvasId, ...) = User.prepareSceneData();

//保存某個場景數據到引擎中
EngineJsAPI.setXXXSceneData(canvasId, ...);

EngineJsAPI.進行初始化();
EngineJsAPI.開啓主循環();

識別最小3D程序的初始化包含的用戶邏輯和引擎邏輯

初始化對應的通用語言爲:
此處輸入圖片的描述

最小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,
    );

用戶邏輯

咱們能夠先識別出下面的用戶邏輯:

  • 準備canvas的id
  • 調用API,傳入canvas的id
  • 準備webgl上下文的配置項

用戶須要傳入webgl上下文的配置項到引擎中。
咱們進行相關的思考:
引擎應該增長一個傳入配置項的API嗎?
配置項應該保存到引擎中嗎?

考慮到:

  • 該配置項只被使用一次,即在「得到webgl上下文」時才須要使用配置項
  • 「得到webgl上下文」是在「初始化」的時候進行

因此引擎不須要增長API,也不須要保存配置項,而是在「進行初始化」的API中傳入「配置項」,使用一次後即丟棄。

引擎邏輯

  • 得到canvas
  • 雖然不用保存配置項,可是要根據配置項和canvas,保存從canvas得到的webgl的上下文

二、初始化全部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組一一關聯。

更新後的三角形通用語言爲:
此處輸入圖片的描述

根據以上的分析,咱們識別出下面的用戶邏輯:

  • 準備兩個Shader名稱
  • 準備兩組GLSL
  • 調用API,傳入一個三角形的Shader名稱
    用戶須要調用該API三次,從而把全部三角形的Shader名稱都傳入引擎
  • 調用API,傳入一個Shader名稱和關聯的GLSL組
    用戶須要調用該API兩次,從而把全部Shader的Shader名稱和GLSL組都傳入引擎

引擎邏輯

咱們如今來思考如何解決下面的不足之處:

存在重複代碼:
1)在_init函數的「初始化全部Shader」中有重複的模式

解決方案:
一、得到全部Shader的Shader名稱和GLSL組集合
二、遍歷這個集合:
1)建立Program
2)初始化Shader

這樣的話,就只須要寫一份「初始化每一個Shader」的代碼了,消除了重複。

根據以上的分析,咱們識別出下面的引擎邏輯:

  • 得到全部Shader的Shader名稱和GLSL組集合
  • 遍歷這個集合
    • 建立Program
    • 初始化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),
  );

用戶邏輯

  • 調用API,準備三個三角形的頂點數據
    由於每一個三角形的頂點數據都同樣,因此應該由引擎負責建立三角形的頂點數據,而後由用戶調用三次API來準備三個三角形的頂點數據
  • 調用API,傳入三個三角形的頂點數據
  • 準備三個三角形的位置數據
  • 準備三個三角形的顏色數據
  • 準備相機數據
    準備view matrix須要的eye、center、up向量和projection matrix須要的near、far、fovy、aspect
  • 調用API,傳入相機數據

引擎邏輯

  • 建立三角形的頂點數據
  • 保存三個三角形的頂點數據
  • 保存三個三角形的位置數據
  • 保存三個三角形的顏色數據
  • 建立和初始化三個三角形的VBO
  • 保存相機數據
    保存eye、center、up向量和near、far、fovy、aspect

識別最小3D程序的主循環包含的用戶邏輯和引擎邏輯

主循環對應的通用語言爲:
此處輸入圖片的描述

對應最小3D程序的_loop函數對應主循環,如今依次分析主循環的每一個步驟對應的代碼:

一、開啓主循環
相關代碼爲:

let rec _loop = data =>
  DomExtend.requestAnimationFrame((time: float) => {
    _loopBody(data);
    _loop(data) |> ignore;
  });

用戶邏輯

引擎邏輯

  • 調用requestAnimationFrame開啓主循環

如今進入_loopBody函數:
二、設置清空顏色緩衝時的顏色值
相關代碼爲:

let _clearColor = ((gl, sceneData) as data) => {
  WebGL1.clearColor(0., 0., 0., 1., gl);

  data;
};

let _loopBody = data => {
  data |> ... |> _clearColor |> ...
};

用戶邏輯

  • 準備清空顏色緩衝時的顏色值
  • 調用API,傳入清空顏色緩衝時的顏色值

引擎邏輯

  • 保存清空顏色緩衝時的顏色值
  • 設置清空顏色緩衝時的顏色值

三、清空畫布
相關代碼爲:

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);

用戶邏輯

引擎邏輯

  • 設置WebGL狀態

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));

用戶邏輯

引擎邏輯

  • 計算view matrix
  • 計算projection matrix

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);

用戶邏輯

引擎邏輯

  • 計算三個三角形的model matrix

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,
  );

用戶邏輯

引擎邏輯

  • 根據第一個三角形的Shader名稱,得到關聯的Program
  • 渲染第一個三角形
    • 使用對應的Program
    • 傳遞三角形的頂點數據
    • 傳遞view matrix和projection matrix
    • 傳遞三角形的model matrix
    • 傳遞三角形的顏色數據
    • 繪製三角形
      • 根據indices計算頂點個數,做爲drawElements的第二個形參

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
    index.html頁面是引擎的用戶

咱們把用戶邏輯中須要用戶實現的邏輯移到角色「index.html」中;
把用戶邏輯中須要調用API實現的邏輯做爲用例,移到角色「引擎」中。
獲得的用例圖以下所示:
此處輸入圖片的描述

設計架構,給出架構視圖

咱們使用四層的分層架構,架構視圖以下所示:
此處輸入圖片的描述

不容許跨層訪問。

對於「API層」和「應用服務層」,咱們會在給出領域視圖後,詳細設計它們。

咱們加入了「倉庫」,使「實體」只能經過「倉庫」來操做「數據」,隔離「數據」和「實體」。
只有「實體」負責持久化數據,因此只有「實體」依賴「倉庫」,「值對象」和「領域服務」都不該該依賴「倉庫」。

之因此「倉庫」依賴了「領域服務」、「實體」、「值對象」,是由於「倉庫」須要調用它們的函數,實現「數據」的PO和領域層的DO之間的轉換。

對於「倉庫」、「數據」、PO、DO,咱們會在後面的「設計數據」中詳細分析。

分析「基礎設施層」的「外部」

「外部」負責與引擎的外部交互。
它包含兩個部分:

  • Js庫
    使用FFI封裝引擎調用的Js庫。
  • 外部對象
    使用FFI定義外部對象,如:
    最小3D程序的DomExtend.re能夠放在這裏,由於它依賴了「window」這個外部對象;
    Utils.re的error函數也能夠放在這裏,由於它們依賴了「js異常」這個外部對象。

劃分引擎子域和限界上下文

以下圖所示:
此處輸入圖片的描述

給出限界上下文映射圖

以下圖所示:
此處輸入圖片的描述

其中:

  • 「U」爲上游,「D」爲下游
    下游依賴上游
  • 「C」爲遵奉者
  • 「CSD」爲客戶方——供應方開發
  • 「OHS」爲開放主機服務
  • 「PL」爲發佈語言
  • 「ACL」爲防腐層

上下文關係的介紹詳見上下文映射圖

如今咱們來分析下防腐層(ACL)的設計,其中相關的領域模型會在後面的「領域視圖」中給出。

「初始化全部Shader」限界上下文的防腐設計

一、「着色器」限界上下文提供着色器的DO數據
二、「初始化全部Shader」限界上下文的領域服務BuildInitShaderData做爲防腐層,將着色器DO數據轉換爲值對象InitShader
三、「初始化全部Shader」限界上下文的領域服務InitShader遍歷值對象InitShader,初始化每一個Shader

經過這樣的設計,隔離了領域服務InitShader和「着色器」限界上下文。

設計值對象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和「場景圖」限界上下文。

設計值對象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的類型定義

經過前面對渲染數據的分析,能夠給出值對象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);

識別領域概念

識別出新的領域概念:

  • Transform
    咱們識別出「Transform」的概念,用它來在座標系中定位三角形。
    Transform的數據包括三角形的位置、旋轉和縮放。在當前場景中,Transform數據 = 三角形的位置
  • Geometry
    咱們識別出「Geometry」的概念,用它來表達三角形的形狀。
    Geometry的數據包括三角形的頂點數據和VBO。在當前場景中,Geometry數據 = 三角形的Vertices、Indices和對應的VBO
  • Material
    咱們識別出「Material」的概念,用它來表達三角形的材質。
    Material的數據包括三角形的着色器、顏色、紋理、光照。在當前場景中,Material數據 = 三角形的Shader名稱 + 三角形的顏色

創建領域模型,給出領域視圖

領域視圖以下所示,圖中包含了領域模型之間的全部聚合、組合關係,以及領域模型之間的主要依賴關係
此處輸入圖片的描述

設計數據

分層數據視圖

以下圖所示:
此處輸入圖片的描述

設計PO Container

PO Container做爲一個容器,負責保存PO到內存中。

PO Container應該爲一個全局Record,有一個可變字段po,用於保存PO

相關的設計爲:

type poContainer = {
  mutable po
};

let poContainer = {
  po: 建立PO()
};

這裏有兩個壞味道:

  • poContainer爲全局變量
    這是爲了讓poContainer在程序啓動到終止期間,一直存在於內存中
  • 使用了可變字段po
    這是爲了在設置PO到poContainer中時,讓poContainer在內存中始終只有一份

咱們應該儘可能使用局部變量和不可變數據/不可變操做,消除共享的狀態。但有時候壞味道不可避免,所以咱們使用下面的策略來處理壞味道:

  • 把壞味道集中和隔離到一個可控的範圍
  • 使用容器來封裝反作用
    如函數內部發生錯誤時,能夠用容器來包裝錯誤信息,返回給函數外部,在外部的某處(可控的範圍)集中處理錯誤。詳見後面的「使用Result處理錯誤」

設計PO

咱們設計以下:

  • 用Record做爲PO的數據結構
  • PO的字段對應聚合根的數據
  • PO是不可變數據

相關的設計爲:

type po = {
    //各個聚合根的數據
    
    canvas,
    shaderManager,
    scene,
    context,
    vboManager
};

由於如今信息不夠,因此不設計聚合根的具體數據,留到實現時再設計它們。

設計容器管理

容器管理負責讀/寫PO Container的PO,相關設計以下:

type getPO = unit => po;
type setPO = po => unit;

設計倉庫

職責

  • 未來自領域層的DO轉換爲PO,設置到PO Container中
  • 從PO Container中得到PO,轉換爲DO傳遞給領域層

僞代碼和類型簽名

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;
};

設計API層

職責

  • 將index.html輸入的VO轉換爲DTO,傳遞給應用服務層
  • 將應用服務層輸出的DTO轉換爲VO,返回給用戶index.html

API層的用戶的特色

用戶爲index.html頁面,它只知道javascript,不知道Reason

引擎API的設計原則

咱們根據用戶的特色,決定設計原則:

  • 應該對用戶隱藏API層下面的層級
    如:
    用戶不該該知道基礎設施層的「數據」的存在。
  • 應該對用戶隱藏實現的細節
    如:
    用戶須要一個API來得到canvas,而引擎API經過「非純」操做來得到canvas並返回給用戶。
    用戶不須要知道是怎樣得到canvas的,因此API的名稱應該爲getCanvas,而不該該爲unsafeGetCanvas(在引擎中,若是咱們經過「非純」操做得到了某個值,則稱該操做爲unsafe)
  • 輸入和輸出應該爲VO,而VO的類型爲javascript的數據類型
    • 應該對用戶隱藏Reason語言的語法
      如:
      不該該對用戶暴露Reason語言的Record等數據結構,但能夠對用戶暴露Reason語言的Tuple,由於它與javascript的數組類型相同
    • 應該對用戶隱藏Reason語言的類型
      如:
      API的輸入參數和輸出結果應該爲javascript的數據類型,不能爲Reason獨有的類型
      (
      Reason的string,int等類型與javascript的數據類型相同,能夠做爲API的輸入參數和輸出結果;
      可是Reason的Discriminated Union類型抽象類型等類型是Reason獨有的,不能做爲API的輸入參數和輸出結果。
      )

劃分API模塊,設計具體的API

首先根據用例圖的用例,劃分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;
};

設計應用服務層

職責

  • 將API層輸入的DTO轉換爲DO,傳遞給領域層
  • 將領域層輸出的DO轉換爲DTO,返回給API層
  • 處理錯誤

設計應用服務

咱們進行下面的設計:

  • API層模塊與應用服務層的應用服務模塊一一對應
  • API與應用服務的函數一一對應

目前來看,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;
};

使用Result處理錯誤

咱們在從0開發3D引擎(五):函數式編程及其在引擎中的應用中介紹了「使用Result來處理錯誤」,它相比「拋出異常」的錯誤處理方式,有不少優勢。

咱們在引擎中主要使用Result來處理錯誤。可是在後面的「優化」中,咱們能夠看到爲了優化,引擎也使用了「拋出異常」的錯誤處理方式。

使用「Discriminated Union類型」來增強值對象的值類型約束

咱們以值對象Matrix爲例,來看下如何增強值對象的值類型約束,從而在編譯檢查時確保類型正確:
Matrix的值類型爲Js.Typed_array.Float32Array.t,這樣的類型設計有個缺點:不能與其它Js.Typed_array.Float32Array.t類型的變量區分開。

所以,在Matrix中可使用Discriminated Union類型來定義「Matrix」類型:

type t =
  | Matrix(Js.Typed_array.Float32Array.t);

這樣就能解決該缺點了。

優化

咱們在性能熱點處進行下面的優化:

  • 處理錯誤優化
    由於使用「拋出異常」的方式處理錯誤不須要操做容器Result,性能更好,因此在性能熱點處:
    使用「拋出異常」的方式處理錯誤,而後在上一層使用Result.tryCatch將異常轉換爲Result
    在其它地方:
    直接用Result包裝錯誤信息
  • Discriminated Union類型優化
    由於操做「Discriminated Union類型」須要操做容器,性能較差,因此在性能熱點處:
    一、在性能熱點開始前,經過一次遍歷操做,將全部相關的值對象的值從「Discriminated Union類型」中取出來。其中取出的值是primitive類型,即int、string等沒有用容器包裹的原始類型
    二、在性能熱點處操做primtive類型的值
    三、在性能熱點結束後,經過一次遍歷操做,將更新後的primitive類型的值寫到「Discriminated Union類型」中

哪些地方屬於性能熱點呢?
咱們須要進行benchmark測試來肯定性能熱點,不過通常來講下面的場景屬於性能熱點的機率比較大:

  • 遍歷數量大的集合
    如遍歷場景中全部的三角形,由於一般場景有至少上千個模型。
  • 雖然遍歷數量小的集合,但每次遍歷的時間或內存開銷大
    如遍歷場景中全部的Shader,由於一般場景有隻幾十個到幾百個Shader,數量不是不少,可是在每次遍歷時會初始化Shader,形成較大的時間開銷。

具體來講,目前引擎的適用於此處提出的優化的性能熱點爲:

  • 初始化全部Shader時,優化「遍歷和初始化每一個Shader」
    優化的僞代碼爲:
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程序中提煉引擎。

相關文章
相關標籤/搜索