编程语言
首页 > 编程语言> > 从0开发3D引擎(十):使用领域驱动设计,从最小3D程序中提炼引擎(上)

从0开发3D引擎(十):使用领域驱动设计,从最小3D程序中提炼引擎(上)

作者:互联网

目录

大家好,本文使用领域驱动设计的方法,重新设计最小3D程序,识别出“用户”和“引擎”角色,给出各种设计的视图。

上一篇博文

从0开发3D引擎(九):实现最小的3D程序-“绘制三角形”

下一篇博文

前置知识

从0开发3D引擎(补充):介绍领域驱动设计

回顾上文

上文获得了下面的成果:
1、最小3D程序
2、领域驱动设计的通用语言

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

Book-Demo-Triangle Github Repo

通用语言

此处输入图片的描述

将会在本文解决的不足之处

1、场景逻辑和WebGL API的调用逻辑混杂在一起
2、存在重复代码:
1)在_init函数的“初始化所有Shader”中有重复的模式
2)在_render中,渲染三个三角形的代码非常相似
3)Utils的sendModelUniformData1和sendModelUniformData2有重复的模式
3、_init传递给主循环的数据过于复杂

本文流程

我们根据上文的成果,进行下面的设计:
1、识别最小3D程序的用户逻辑和引擎逻辑
2、根据用户逻辑,给出用例图,用于设计API
3、设计分层架构,给出架构视图
4、进行领域驱动设计的战略设计
1)划分引擎子域和限界上下文
2)给出限界上下文映射图
5、进行领域驱动设计的战术设计
1)识别领域概念
2)建立领域模型,给出领域视图
6、设计数据,给出数据视图
7、根据用例图,设计分层架构的API层
8、根据API层的设计,设计分层架构的应用服务层
9、进行一些细节的设计:
1)使用Result处理错误
2)使用“Discriminated Union类型”来加强值对象的值类型约束
10、基本的优化

解释本文使用的领域驱动设计的一些概念

//定义聚合根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

识别最小3D程序的顶层包含的用户逻辑和引擎逻辑

从顶层来看,包含三个部分的逻辑:创建场景、初始化、主循环

我们依次识别它们的用户逻辑和引擎逻辑:
1、创建场景
用户逻辑

引擎逻辑

2、初始化

用户逻辑

引擎逻辑

3、主循环

用户逻辑

引擎逻辑

根据对最小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函数负责初始化

现在依次分析初始化的每个步骤对应的代码:
1、获得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中传入“配置项”,使用一次后即丢弃。

引擎逻辑

2、初始化所有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”中有重复的模式

解决方案:
1、获得所有Shader的Shader名称和GLSL组集合
2、遍历这个集合:
1)创建Program
2)初始化Shader

这样的话,就只需要写一份“初始化每个Shader”的代码了,消除了重复。

根据以上的分析,我们识别出下面的引擎逻辑:

3、初始化场景
相关代码为:

  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程序的主循环包含的用户逻辑和引擎逻辑

主循环对应的通用语言为:
此处输入图片的描述

对应最小3D程序的_loop函数对应主循环,现在依次分析主循环的每个步骤对应的代码:

1、开启主循环
相关代码为:

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

用户逻辑

引擎逻辑

现在进入_loopBody函数:
2、设置清空颜色缓冲时的颜色值
相关代码为:

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

  data;
};

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

用户逻辑

引擎逻辑

3、清空画布
相关代码为:

let _clearCanvas = ((gl, sceneData) as data) => {
  WebGL1.clear(
    WebGL1.getColorBufferBit(gl) lor WebGL1.getDepthBufferBit(gl),
    gl,
  );

  data;
};

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

用户逻辑

引擎逻辑

4、渲染

相关代码为:

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)的设计,其中相关的领域模型会在后面的“领域视图”中给出。

“初始化所有Shader”限界上下文的防腐设计

1、“着色器”限界上下文提供着色器的DO数据
2、“初始化所有Shader”限界上下文的领域服务BuildInitShaderData作为防腐层,将着色器DO数据转换为值对象InitShader
3、“初始化所有Shader”限界上下文的领域服务InitShader遍历值对象InitShader,初始化每个Shader

通过这样的设计,隔离了领域服务InitShader和“着色器”限界上下文。

设计值对象InitShader

根据识别的引擎逻辑,可以得知值对象InitShader的值是所有Shader的Shader名称和GLSL组集合,因此我们可以给出值对象InitShader的类型定义:

type singleInitShader = {
  shaderId: string,
  vs: string,
  fs: string,
};

//值对象InitShader类型定义
type initShader = list(singleInitShader);

“渲染”限界上下文的防腐设计

1、“场景图”限界上下文提供场景图的DO数据
2、“渲染”限界上下文的领域服务BuildRenderData作为防腐层,将场景图DO数据转换为值对象Render
3、“渲染”限界上下文的领域服务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、存在重复代码:
...
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有重复的模式”

那是否可以把所有三角形的颜色数据统一用一个数据结构来保存,然后在渲染三角形->传递三角形的颜色数据时,遍历该数据结构,只用一个函数(而不是两个函数:sendModelUniformData1、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);

识别领域概念

识别出新的领域概念:

建立领域模型,给出领域视图

领域视图如下所示,图中包含了领域模型之间的所有聚合、组合关系,以及领域模型之间的主要依赖关系
此处输入图片的描述

设计数据

分层数据视图

如下图所示:
此处输入图片的描述

设计PO Container

PO Container作为一个容器,负责保存PO到内存中。

PO Container应该为一个全局Record,有一个可变字段po,用于保存PO

相关的设计为:

type poContainer = {
  mutable po
};

let poContainer = {
  po: 创建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;
};

设计API层

职责

API层的用户的特点

用户为index.html页面,它只知道javascript,不知道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;
};

设计应用服务层

职责

设计应用服务

我们进行下面的设计:

目前来看,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);

这样就能解决该缺点了。

优化

我们在性能热点处进行下面的优化:

哪些地方属于性能热点呢?
我们需要进行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、分层架构视图和每一层的设计
3、领域驱动设计的战略成果
1)引擎子域和限界上下文划分
2)限界上下文映射图
4、领域驱动设计的战术成果
1)领域概念
2)领域视图
5、数据视图和PO的相关设计
6、一些细节的设计
7、基本的优化

本文解决了上文的不足之处:

1、场景逻辑和WebGL API的调用逻辑混杂在一起

本文识别出用户index.html和引擎这两个角色,分离了用户逻辑和引擎,从而解决了这个不足

2、存在重复代码:
1)在_init函数的“初始化所有Shader”中有重复的模式
2)在_render中,渲染三个三角形的代码非常相似
3)Utils的sendModelUniformData1和sendModelUniformData2有重复的模式

本文提出了值对象InitShader和值对象Render,分别用一份代码实现“初始化每个Shader”和“渲染每个三角形”,然后分别在遍历对应的值对象时调用对应的一份代码,从而消除了重复

3、_init传递给主循环的数据过于复杂

本文对数据进行了设计,将数据分为VO、DTO、DO、PO,从而不再传递数据,解决了这个不足

本文不足之处

1、仓库与领域模型之间存在循环依赖
2、没有隔离基础设施层的“数据”的变化对领域层的影响
如在支持多线程时,需要增加渲染线程的数据,则不应该影响支持单线程的相关代码
3、没有隔离“WebGL”的变化
如在支持WebGL2时,不应该影响支持WebGL1的代码

下文概要

在下文中,我们会根据本文的成果,具体实现从最小的3D程序中提炼引擎。

标签:API,数据,float,提炼,引擎,三角形,gl,type,3D
来源: https://www.cnblogs.com/chaogex/p/12408831.html