<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      從0開發(fā)3D引擎(十):使用領(lǐng)域驅(qū)動設(shè)計,從最小3D程序中提煉引擎(第一部分)

      目錄

      大家好,本文使用領(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é)果。
          )

      劃分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程序中提煉引擎。

      posted @ 2020-03-04 12:52  楊元超  閱讀(756)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 麻豆国产va免费精品高清在线| 亚洲中文无码永久免费| 色综合欧美亚洲国产| 大地资源免费视频观看| 息烽县| 在线精品另类自拍视频| 少妇被日自拍黄色三级网络| 美女自卫慰黄网站| 日韩人妻无码中文字幕视频| 亚洲国产精品一二三四五| 中文字幕av一区二区| 精品少妇av蜜臀av| 国产精品中文字幕一区| 精品亚洲国产成人| 在线看免费无码的av天堂| 动漫精品中文无码卡通动漫| 熟女精品色一区二区三区| 免费人成视频在线观看网站| 日韩人妻无码一区二区三区99| 强奷白丝美女在线观看| 亚洲av第三区国产精品| 欧美黑人巨大xxxxx| 99在线视频免费观看| 亚洲男人的天堂网站| 香港日本三级亚洲三级| 日韩一区二区a片免费观看| 亚洲精品入口一区二区乱| 午夜福利院一区二区三区| 国产精品国产三级国产专| 国精偷拍一区二区三区| 亚洲精品一区二区制服| 天天躁日日躁狠狠躁中文字幕| 丰满人妻熟妇乱又伦精品劲| 午夜性爽视频男人的天堂| 亚洲av永久无码精品水牛影视| 国产精品一线天粉嫩av| 少妇被黑人到高潮喷出白浆| 人妻系列中文字幕精品| 虎白女粉嫩尤物福利视频| 在线播放深夜精品三级| 少妇激情a∨一区二区三区 |