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

大家好,本文根据领域驱动设计的成果,实现了start API,完成了实现,更新了领域视图。

上一篇博文

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

继续实现

实现“DirectorJsAPI.start”

实现“主循环”

1、修改DirectorJsAPI.re,实现API
DirectorJsAPI.re代码为:

let start = DirectorApService.start;

2、实现主循环
1)修改DomExtend.re,定义requestAnimationFrame的FFI
DomExtend.re相关代码为:

[@bs.val] external requestAnimationFrame: (float => unit) => int = "";

2)修改DirectorApService.re,实现主循环
DirectorApService.re相关代码为:

let _loopBody = () => {
  //暂时没有逻辑
  ResultContainerVO.succeed();
};

let rec _loop = () =>
  DomExtend.requestAnimationFrame((time: float) => {
    //用抛出异常的方式处理Result错误
    _loopBody() |> ResultContainerVO.handleFail(Error.throwError) |> ignore;

    _loop() |> ignore;
  });

let start = () => {
  _loop();
};

实现“设置清空颜色缓冲时的颜色值”限界上下文

1、修改DirectorApService,重写_loopBody函数
DirectorApService.re相关代码为:

let _loopBody = () => {
  ClearColorClearColorDoService.clearColor()
};

2、在src/domain_layer/domain/loop/clear_color/service/中加入ClearColorClearColorDoService.re,创建领域服务ClearColor
ClearColorClearColorDoService.re代码为:

let clearColor = (): ResultContainerVO.t(unit, Js.Exn.t) => {
  ContextContextEntity.getGl()
  |> ResultContainerVO.mapSuccess(gl => {ContextContextEntity.clearColor(gl)});
};

3、修改ContextContextEntity.re,实现clearColor函数
ContextContextEntity.re相关代码为:

let clearColor = gl => {
  let (r, g, b, a) = ContextRepo.getClearColor();

  WebGL1.clearColor(r, g, b, a, gl);
};

实现“清空画布”限界上下文

1、修改DirectorApService,重写_loopBody函数
DirectorApService.re相关代码为:

let _loopBody = () => {
  ClearColorClearColorDoService.clearColor()
  |> ResultContainerVO.bind(() => {ClearClearCanvasDoService.clearCanvas()});
};

2、在src/domain_layer/domain/loop/clear_canvas/service/中加入ClearCanvasClearCanvasDoService.re,创建领域服务ClearCanvas
ClearCanvasClearCanvasDoService.re代码为:

let clearCanvas = (): ResultContainerVO.t(unit, Js.Exn.t) => {
  ContextContextEntity.getGl()
  |> ResultContainerVO.mapSuccess(gl => {
       ContextContextEntity.clearCanvas(gl)
     });
};

3、修改ContextContextEntity.re,实现clearCanvas函数
ContextContextEntity.re相关代码为:

let clearCanvas = gl => {
  WebGL1.clear(
    WebGL1.getColorBufferBit(gl) lor WebGL1.getDepthBufferBit(gl),
    gl,
  );
};

实现“渲染”限界上下文

1、修改DirectorApService,重写_loopBody函数
DirectorApService.re相关代码为:

let _loopBody = () => {
  ClearColorClearColorDoService.clearColor()
  |> ResultContainerVO.bind(() => {
       ClearCanvasClearCanvasDoService.clearCanvas()
       |> ResultContainerVO.bind(() => {RenderRenderDoService.render()})
     });
};

下面,我们依次实现“渲染”的引擎逻辑:

实现“设置WebGL状态”

1、在src/domain_layer/domain/loop/render/service/中加入RenderRenderDoService.re,创建领域服务Render
RenderRenderDoService.re代码为:

let _initGlState = gl => {
  ContextContextEntity.enableDepthTest(gl);
  ContextContextEntity.enableBackCullFace(gl);
};

let render = () => {
  ContextContextEntity.getGl()
  |> ResultContainerVO.bind(gl => {
       _initGlState(gl) |> ResultContainerVO.succeed
     });
};

2、修改ContextContextEntity.re,实现相关函数
ContextContextEntity.re相关代码为:

let enableDepthTest = gl => gl |> WebGL1.enable(WebGL1.getDepthTest(gl));

let enableBackCullFace = gl => {
  WebGL1.enable(WebGL1.getCullFace(gl), gl);
  WebGL1.cullFace(WebGL1.getBack(gl), gl);
};

实现“创建和初始化三个三角形的VBO”

1、设计值对象Geometry的DO中与VBO相关的字段

根据领域模型:
此处输入图片的描述

在领域模型中,Geometry组合了一个VBO,这应该如何体现到Geometry的DO中?
这个与前面我们设计值对象Material的DO(Material组合了一个Shader)类似,解决方案为:
1)实体VBO的DO只包含VBO的id,其中id为一个从0开始不断加1的int类型
2)Geometry DO包含一个VBO的id

这样就使Geometry通过VBO的id,与VBO关联起来了!

因此我们可以修改Geometry的DO,加入vbo字段:

//值对象Geometry的DO
type t = {
  ...
  //因为在创建值对象Geometry时,它的VBO还没有创建,在渲染时才会创建它的VBO,所以vbo字段为option类型
  vbo: option(VBO DO),
};

2、设计“VBO管理”限界上下文的DO

根据领域模型:
此处输入图片的描述

我们来设计相关的DO:
1)设计实体VBO、值对象VertexBuffer、值对象IndexBuffer的DO
VBO DO只包含VBO的id:

type id = int;

type t =
  | VBO(id);

VertexBuffer DO

type t =
  | VertexBuffer(WebGL1.buffer);

IndexBuffer DO

type t =
  | IndexBuffer(WebGL1.buffer);

2)设计聚合根VBOManager的DO

VBO的id逻辑为:创建实体VBO时,将最大的VBO id加1,作为它的id。
为了实现该逻辑,需要在VBOManager DO中保存最大的VBO id:

type t = {
  maximumId: VBO DO,
};

我们需要根据VBO id,获得它的值对象VertexBuffer和值对象IndexBuffer的DO,所以类似于聚合根ShaderManager DO的programMap,VBOManager DO需要两个map,它们的key是VBO id,value分别为值对象VertexBuffer的DO和值对象IndexBuffer的DO。
这里值得注意的是:这两个map并不是hash map,因为它的key为int而不是string类型!所以我们需要加入sparse map。sparse map和hash map的区别就是map的key的类型不同。

应该在领域视图的“容器”限界上下文中,加入值对象ImmutableSparseMap、值对象MutableSparseMap,其中ImmutableSparseMap用于实现不可变的sparse map,MutableSparseMap用于实现可变的sparse map。

我们先设计它们的DO,它们的DO是一样的:
ImmutableSparseMap DO

type t('index, 'value) = array('value);

MutableSparseMap DO

type t('index, 'value) = array('value);

现在我们可以设计出聚合根VBOManager完整的DO:

type t = {
  maximumId: VBO DO,
  vertexBufferMap:ImmutableSparseMap.t(VBO DO, VertexBuffer DO)
  indexBufferMap:ImmutableSparseMap.t(VBO DO, IndexBuffer DO),
};

3、实现sparse map
类似于实现hash map:
1)在src/domain_layer/domain/structure/container/value_object/中创建文件夹sparse_map/
2)在sparse_map/文件夹中加入ImmutableSparseMapContainerVO.re、MutableSparseMapContainerVO.re、SparseMapContainer.re、SparseMapContainerType.re
ImmutableSparseMapContainerVO.re负责实现Immutable Sparse Map;
MutableSparseMapContainerVO.re负责实现Mutable Sparse Map;
SparseMapContainer.re从两者中提出的公共代码;
SparseMapContainerType.re定义SparseMap的类型和相关的类型转换。

相关代码如下:
SparseMapContainerType.re

type t('index, 'value) = array('value);
type t2('value) = t(int, 'value);

external notNullableToNullable: 'a => Js.Nullable.t('a) = "%identity";

SparseMapContainer.re

let createEmpty = (): SparseMapContainerType.t2('a) => [||];

let copy = Js.Array.copy;

let _unsafeGet = (index: int, map: SparseMapContainerType.t2('a)): 'a => {
  Array.unsafe_get(map, index);
};

let _isEmpty = (value: 'a): bool =>
  value |> SparseMapContainerType.notNullableToNullable |> Js.Nullable.test;

let get = (index: int, map) => {
  let value = _unsafeGet(index, map);
  _isEmpty(value) ? None : Some(value);
};

let has = (index: int, map) => !_isEmpty(_unsafeGet(index, map));

ImmutableSparseMapContainerVO.re

type t('index, 'value) = SparseMapContainerType.t('index, 'value);

let createEmpty = SparseMapContainer.createEmpty;

let copy = SparseMapContainer.copy;

let get = SparseMapContainer.get;

let has = SparseMapContainer.has;

let set =
    (key: int, value: 'a, map: SparseMapContainerType.t2('a))
    : SparseMapContainerType.t2('a) => {
  let newMap = map |> copy;

  Array.unsafe_set(newMap, key, value);

  newMap;
};

MutableSparseMapContainerVO.re

type t('index, 'value) = SparseMapContainerType.t('index, 'value);

let createEmpty = SparseMapContainer.createEmpty;

let copy = SparseMapContainer.copy;

let get = SparseMapContainer.get;

let has = SparseMapContainer.has;

let set =
    (key: int, value: 'a, map: SparseMapContainerType.t2('a))
    : SparseMapContainerType.t2('a) => {
  Array.unsafe_set(map, key, value);

  map;
};

4、修改RenderRenderDoService.re,用伪代码实现“创建和初始化三个三角形的VBO”
RenderRenderDoService.re相关伪代码为:

let _initVBOs = (...) => {
  从Scene PO中获得所有三角形的Geometry DO
  |> 遍历((每个三角形的Geometry DO) => {
          let vbo:option(VBO DO) = 获得每个三角形的Geometry DO的vbo字段;

          if(已经创建和初始化了vbo,即vbo为Some){
            continue;
          }
          else{
            创建VBO和它的值对象
            初始化创建的VBO
            加入创建的VBO DO和它的值对象的DO到PO中
            将创建的VBO DO设置到Scene PO->该三角形->Geometry数据->vbo字段中
          }
        })
};

let render = () => {
  ContextContextEntity.getGl()
  |> ResultContainerVO.bind(gl => {
       ...
       
       _initVBOs(...);
     });
};

5、修改RenderRenderDoService.re,具体实现“创建和初始化三个三角形的VBO”
RenderRenderDoService.re相关代码为:

let _initVBOs = gl => {
  SceneSceneGraphEntity.getAllTriangles()
  |> List.iteri((triangleIndex, {geometry}: TriangleSceneGraphVO.t) => {
       let {vbo, vertices, indices}: GeometrySceneGraphVO.t = geometry;

       GeometrySceneGraphVO.hasVBO(vbo)
         ? {
           ();
         }
         : {
           let vbo = VBOManagerVBOManagerEntity.createVBO();

           (
             VBOVBOManagerEntity.createVertexBuffer(gl),
             VBOVBOManagerEntity.createIndexBuffer(gl),
           )
           |> VBOManagerVBOManagerEntity.initVBO(gl, (vertices, indices))
           |> VBOManagerVBOManagerEntity.addVBO(vbo);

           /* setVBO函数负责实现“将VBO DO设置到Scene PO->该三角形->Geometry数据->vbo字段中”
           
           这里需要思考的是:在哪里实现setVBO函数呢?

           因为该函数需要使用仓库来设置到Scene PO中,而我们应该只在实体中使用仓库,所以该函数应该放在聚合根Scene而不是值对象Triangle或者值对象Geometry中。
           另外,为了在设置时定位到Scene PO对应的三角形,需要该三角形在Scene DO的triangles字段(为一个list集合)中的序号triangleIndex。
           */
           SceneSceneGraphEntity.setVBO(vbo, triangleIndex);
         };
     });
};

let render = () => {
  ContextContextEntity.getGl()
  |> ResultContainerVO.bind(gl => {
       ...

       _initVBOs(gl) |> ResultContainerVO.succeed;
  });
});

5、修改GeometrySceneGraphVO.re,Geometry DO加入vbo字段,实现hasVBO函数,修改create函数

type t = {
  vertices: VerticesSceneGraphVO.t,
  indices: IndicesSceneGraphVO.t,
  vbo: option(VBOVBOManagerEntity.t),
};

let create = (vertices, indices): ScenePOType.geometry => {
  ...
  vbo: None,
};

let hasVBO = vbo => {
  vbo |> OptionContainerDoService.isSome;
};

6、修改OptionContainerDoService.re,实现isSome函数
OptionContainerDoService.re相关代码为:

let isSome = Js.Option.isSome;

7、修改SceneSceneGraphEntity.re和相关的仓库,实现getAllTriangles函数
SceneSceneGraphEntity.re相关代码为:

let getAllTriangles = () => {
  SceneRepo.getAllTriangles();
};

SceneRepo.re相关代码为:

let getAllTriangles = () => {
  _getTriangles(Repo.getScene()) |> List.map(TriangleSceneRepo.build);
};

TriangleSceneRepo.re相关代码为:

let build =
    ({transform, geometry, material}: ScenePOType.triangle)
    : TriangleSceneGraphVO.t => {
  transform: TransformSceneRepo.build(transform),
  geometry: GeometrySceneRepo.build(geometry),
  material: MaterialSceneRepo.build(material),
};

TransformSceneRepo.re相关代码为:

let build = ({position}: ScenePOType.transform): TransformSceneGraphVO.t => {
  position: position |> VectorMathVO.create |> PositionSceneGraphVO.create,
};

GeometrySceneRepo.re相关代码为:

let build =
    ({vertices, indices, vbo}: ScenePOType.geometry): GeometrySceneGraphVO.t => {
  vertices: vertices |> VerticesSceneGraphVO.create,
  indices: indices |> IndicesSceneGraphVO.create,
  vbo: vbo |> OptionContainerDoService.map(VBOVBOManagerEntity.create),
};

MaterialSceneRepo.re相关代码为:

let build = ({shader, colors}: ScenePOType.material): MaterialSceneGraphVO.t => {
  shader: shader |> ShaderShaderEntity.create,
  colors: colors |> List.map(color => {color |> Color3ContainerVO.create}),
};

8、实现“VBO管理”限界上下文

我们按照下面的步骤来实现:
1)实现相关的领域模型
2)实现相关的PO数据
3)实现相关的仓库

现在来具体实现:
1)在src/domain_layer/domain/webgl_object_manager/vbo_manager/entity/中加入VBOManagerVBOManagerEntity.re,创建聚合根VBOManager
VBOManagerVBOManagerEntity.re代码为:

type t = {
  maximumId: VBOVBOManagerEntity.t,
  vertexBufferMap:
    ImmutableSparseMapContainerVO.t(
      VBOVBOManagerEntity.t,
      VertexBufferVBOManagerVO.t,
    ),
  indexBufferMap:
    ImmutableSparseMapContainerVO.t(
      VBOVBOManagerEntity.t,
      IndexBufferVBOManagerVO.t,
    ),
};

let createVBO = () => {
  let (newId, maximumId) =
    VBOManagerRepo.getMaximumId() |> VBOVBOManagerEntity.generateId;

  VBOManagerRepo.setMaximumId(maximumId);

  newId;
};

let initVBO = (gl, (vertices, indices), (vertexBuffer, indexBuffer)) => {
  (
    VBOVBOManagerEntity.createVertexBuffer(gl)
    |> VBOVBOManagerEntity.initVertexBuffer(gl, vertices),
    VBOVBOManagerEntity.createIndexBuffer(gl)
    |> VBOVBOManagerEntity.initIndexBuffer(gl, indices),
  );
};

let addVBO = (vbo, (vertexBuffer, indexBuffer)) => {
  VBOManagerRepo.addVBO(vbo, vertexBuffer, indexBuffer);
};

2)在src/domain_layer/domain/webgl_object_manager/vbo_manager/entity/中加入VBOVBOManagerEntity.re,创建实体VBO
VBOVBOManagerEntity.re代码为:

type id = int;

type t =
  | VBO(id);

let create = id => VBO(id);

let getId = vbo =>
  switch (vbo) {
  | VBO(id) => id
  };

let mapId = (f, vbo) => vbo |> getId |> f |> create;

let generateId = maximumId => {
  (maximumId, maximumId |> mapId(maximumId => {maximumId |> succ}));
};

let createVertexBuffer = gl => {
  WebGL1.createBuffer(gl) |> VertexBufferVBOManagerVO.create;
};

let createIndexBuffer = gl => {
  WebGL1.createBuffer(gl) |> IndexBufferVBOManagerVO.create;
};

let initVertexBuffer = (gl, vertices, vertexBuffer) => {
  let vertexBuffer = VertexBufferVBOManagerVO.value(vertexBuffer);

  WebGL1.bindBuffer(WebGL1.getArrayBuffer(gl), vertexBuffer, gl);

  WebGL1.bufferFloat32Data(
    WebGL1.getArrayBuffer(gl),
    vertices |> VerticesSceneGraphVO.value,
    WebGL1.getStaticDraw(gl),
    gl,
  );

  vertexBuffer |> VertexBufferVBOManagerVO.create;
};

let initIndexBuffer = (gl, indices, indexBuffer) => {
  let indexBuffer = IndexBufferVBOManagerVO.value(indexBuffer);

  WebGL1.bindBuffer(WebGL1.getElementArrayBuffer(gl), indexBuffer, gl);

  WebGL1.bufferUint16Data(
    WebGL1.getElementArrayBuffer(gl),
    indices |> IndicesSceneGraphVO.value,
    WebGL1.getStaticDraw(gl),
    gl,
  );

  indexBuffer |> IndexBufferVBOManagerVO.create;
};

3)在src/domain_layer/domain/webgl_object_manager/vbo_manager/value_object/中加入VertexBufferVBOManagerVO.re,创建值对象VertexBuffer
VertexBufferVBOManagerVO.re代码为:

type t =
  | VertexBuffer(WebGL1.buffer);

let create = buffer => VertexBuffer(buffer);

let value = buffer =>
  switch (buffer) {
  | VertexBuffer(value) => value
  };

4)在src/domain_layer/domain/webgl_object_manager/vbo_manager/value_object/中加入IndexBufferVBOManagerVO.re,创建值对象IndexBuffer
IndexBufferVBOManagerVO.re代码为:

type t =
  | IndexBuffer(WebGL1.buffer);

let create = buffer => IndexBuffer(buffer);

let value = buffer =>
  switch (buffer) {
  | IndexBuffer(value) => value
  };

5)修改POType.re
POType.re相关代码为:

type po = {
  ...
  vboManager: VBOManagerPOType.vboManager,
};

5)在src/infrastructure_layer/data/po/中加入VBOManagerPOType.re,定义VBOManager PO的类型
VBOManagerPOType.re代码为:

type vboId = int;

type vboManager = {
  maximumId: vboId,
  vertexBufferMap: ImmutableSparseMap.t(vboId, WebGL1.buffer),
  indexBufferMap: ImmutableSparseMap.t(vboId, WebGL1.buffer),
};

这里的vertexBufferMap、indexBufferMap与聚合根VBOManager DO中的vertexBufferMap、indexBufferMap一样,也是sparse map,但不能直接使用领域层的值对象ImmutableSparseContainerVO来定义它的类型!因为PO属于基础设施层,它不能依赖领域层!
因此,与之前我们加入ImmutableHash.re模块类似,我们应该在基础设施层的“数据”中创建一个ImmutableSparseMap.re模块,尽管它的类型和函数都与ImmutableSparseMapContainerVO一样。

在src/infrastructure_layer/data/structure/中加入ImmutableSparseMap.re。
为了方便,目前暂时直接用ImmutableSparseMapContainerVO来实现ImmutableSparseMap,ImmutableSparseMap.re代码为:

type t('index, 'a) = ImmutableSparseMapContainerVO.t('index, 'a);

let createEmpty = ImmutableSparseMapContainerVO.createEmpty;

let get = ImmutableSparseMapContainerVO.get;

let set = ImmutableSparseMapContainerVO.set;

6)在src/domain_layer/repo/中加入VBOManagerRepo.re,实现仓库对VBOManager PO的操作
VBOManagerRepo.re代码为:

open VBOManagerPOType;

let _getMaximumId = ({maximumId}) => maximumId;

let getMaximumId = () => {
  _getMaximumId(Repo.getVBOManager()) |> VBOVBOManagerEntity.create;
};

let setMaximumId = maximumId => {
  Repo.setVBOManager({
    ...Repo.getVBOManager(),
    maximumId: VBOVBOManagerEntity.getId(maximumId),
  });
};

let _getVertexBufferMap = ({vertexBufferMap}) => vertexBufferMap;

let _getIndexBufferMap = ({indexBufferMap}) => indexBufferMap;

let addVBO = (vbo, vertexBuffer, indexBuffer) => {
  Repo.setVBOManager({
    ...Repo.getVBOManager(),
    vertexBufferMap:
      _getVertexBufferMap(Repo.getVBOManager())
      |> ImmutableSparseMap.set(
           VBOVBOManagerEntity.getId(vbo),
           VertexBufferVBOManagerVO.value(vertexBuffer),
         ),

    indexBufferMap:
      _getIndexBufferMap(Repo.getVBOManager())
      |> ImmutableSparseMap.set(
           VBOVBOManagerEntity.getId(vbo),
           IndexBufferVBOManagerVO.value(indexBuffer),
         ),
  });
};

7)修改OptionContainerDoService.re,实现map函数
OptionContainerDoService.re相关代码为:

let map = (func, optionData) =>
  optionData |> Js.Option.map((. data) => func(data));

8)修改Repo.re,实现仓库对VBOManager PO的操作
Repo.re相关代码为:

let getVBOManager = () => {
  let po = ContainerManager.getPO();

  po.vboManager;
};

let setVBOManager = vboManager => {
  let po = ContainerManager.getPO();

  {...po, vboManager} |> ContainerManager.setPO;
};

9)修改CreateRepo.re,实现创建VBOManager PO
CreateRepo.re相关代码为:

let create = () => {
  ...
  vboManager: {
    maximumId: 0,
    vertexBufferMap: ImmutableSparseMap.createEmpty(),
    indexBufferMap: ImmutableSparseMap.createEmpty(),
  },
};

9、实现setVBO函数
我们按照下面的步骤来实现:
1)实现相关的领域模型
2)实现相关的PO数据
3)实现相关的仓库

现在来具体实现:
1)修改SceneSceneGraphEntity.re,实现setVBO函数
SceneSceneGraphEntity.re相关代码为:

let setVBO = (vbo, triangleIndex) => {
  SceneRepo.setVBO(vbo, triangleIndex);
};

2)修改ScenePOType.re,Scene PO的geometry加入vbo字段
ScenePOType.re相关代码为:

type vboId = int;

type geometry = {
  ...
  vbo: option(vboId),
};

3)修改SceneRepo.re,实现setVBO函数
SceneRepo.re相关代码为:

let setVBO = (vbo, targetTriangleIndex) => {
  Repo.setScene({
    ...Repo.getScene(),
    triangles:
      _getTriangles(Repo.getScene())
      |> List.mapi((triangleIndex, triangle) => {
           triangleIndex === targetTriangleIndex
             ? {
               TriangleSceneRepo.setVBO(vbo, triangle);
             }
             : triangle
         }),
  });
};

4)修改TriangleSceneRepo.re,实现setVBO函数
TriangleSceneRepo.re相关代码为:

let setVBO = (vbo, ({geometry}: ScenePOType.triangle) as triangle) => {
  {...triangle, geometry: GeometrySceneRepo.setVBO(vbo, geometry)};
};

5)修改GeometrySceneRepo.re,实现setVBO函数
GeometrySceneRepo.re相关代码为:

let setVBO = (vbo, geometry): ScenePOType.geometry => {
  {...geometry, vbo: Some(vbo |> VBOVBOManagerEntity.getId)};
};

实现“渲染三个三角形”

1、加入值对象Render
从0开发3D引擎(十):使用领域驱动设计,从最小3D程序中提炼引擎(第一部分)的“给出值对象Render的类型定义”中,我们已经定义了值对象Render的类型,所以我们直接将设计转换为实现:
在src/domain_layer/domain/loop/render/value_object/中加入RenderRenderVO.re,创建值对象Render
RenderRenderVO.re代码为:

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;

type t = (gl, camera, triangles);

2、创建领域服务BuildRenderData,构造值对象Render
这里需要思考的问题时:
根据从0开发3D引擎(十):使用领域驱动设计,从最小3D程序中提炼引擎(第一部分)的“渲染”限界上下文的防腐设计,我们知道领域服务BuildRenderData在构造值对象Render时可以获得场景图DO数据(即Scene DO),它包含了_initVBOs函数需要的数据:“所有三角形的Geometry DO”。那么能否在构造值对象Render时执行_initVBOs函数呢?

答案是不能。这是因为在构造值对象Render时,只应该进行读的操作(如读取PO,获得场景图DO数据),而_initVBOs函数需要进行写的操作,所以应该将读写分离,在“构造值对象Render”之前执行_initVBOs函数。

现在我们开始创建领域服务BuildRenderData:
1)创建值对象Matrix
因为构造值对象Render的相机数据(view matrix、projection matrix)时需要创建值对象Matrix,所以需要先实现相关代码。
在src/domain_layer/domain/structure/container/value_object/中加入MatrixContainerVO.re,创建值对象Matrix:

open Js.Typed_array;

type t =
  | Matrix(Float32Array.t);

let createWithoutCheck = resultFloat32Arr => Matrix(resultFloat32Arr);

let createIdentityMatrix = () =>
  Float32Array.make([|
    1.,
    0.,
    0.,
    0.,
    0.,
    1.,
    0.,
    0.,
    0.,
    0.,
    1.,
    0.,
    0.,
    0.,
    0.,
    1.,
  |])
  |> createWithoutCheck;

let value = mat =>
  switch (mat) {
  | Matrix(value) => value
  };

let _getEpsilon = () => 0.000001;

let setLookAt = (eye, center, up, mat) => {
  let eye = eye |> EyeSceneGraphVO.value;
  let (eyeX, eyeY, eyeZ) = eye |> VectorMathVO.value;
  let center = center |> CenterSceneGraphVO.value;
  let (centerX, centerY, centerZ) = center |> VectorMathVO.value;
  let up = up |> UpSceneGraphVO.value;
  let (upX, upY, upZ) = up |> VectorMathVO.value;

  Js.Math.abs_float(eyeX -. centerX) < _getEpsilon()
  && Js.Math.abs_float(eyeY -. centerY) < _getEpsilon()
  && Js.Math.abs_float(eyeZ -. centerZ) < _getEpsilon()
    ? mat
    : {
      let z = VectorMathVO.sub(eye, center) |> VectorMathVO.normalize;

      let x = VectorMathVO.cross(up, z) |> VectorMathVO.normalize;

      let y = VectorMathVO.cross(z, x) |> VectorMathVO.normalize;

      let (x1, x2, x3) = VectorMathVO.value(x);
      let (y1, y2, y3) = VectorMathVO.value(y);
      let (z1, z2, z3) = VectorMathVO.value(z);

      let resultFloat32Arr = value(mat);

      Float32Array.unsafe_set(resultFloat32Arr, 0, x1);
      Float32Array.unsafe_set(resultFloat32Arr, 1, y1);
      Float32Array.unsafe_set(resultFloat32Arr, 2, z1);
      Float32Array.unsafe_set(resultFloat32Arr, 3, 0.);
      Float32Array.unsafe_set(resultFloat32Arr, 4, x2);
      Float32Array.unsafe_set(resultFloat32Arr, 5, y2);
      Float32Array.unsafe_set(resultFloat32Arr, 6, z2);
      Float32Array.unsafe_set(resultFloat32Arr, 7, 0.);
      Float32Array.unsafe_set(resultFloat32Arr, 8, x3);
      Float32Array.unsafe_set(resultFloat32Arr, 9, y3);
      Float32Array.unsafe_set(resultFloat32Arr, 10, z3);
      Float32Array.unsafe_set(resultFloat32Arr, 11, 0.);
      Float32Array.unsafe_set(
        resultFloat32Arr,
        12,
        -. VectorMathVO.dot(x, eye),
      );
      Float32Array.unsafe_set(
        resultFloat32Arr,
        13,
        -. VectorMathVO.dot(y, eye),
      );
      Float32Array.unsafe_set(
        resultFloat32Arr,
        14,
        -. VectorMathVO.dot(z, eye),
      );
      Float32Array.unsafe_set(resultFloat32Arr, 15, 1.);

      resultFloat32Arr |> createWithoutCheck;
    };
};

let buildPerspective = ((fovy, aspect, near, far), mat) => {
  let fovy = FovySceneGraphVO.value(fovy);

  Js.Math.sin(Js.Math._PI *. fovy /. 180. /. 2.) === 0.
    //使用Result处理错误
    ? ResultContainerVO.failWith("frustum should not be null")
    : {
      let aspect = AspectSceneGraphVO.value(aspect);
      let near = NearSceneGraphVO.value(near);
      let far = FarSceneGraphVO.value(far);

      let fovy = Js.Math._PI *. fovy /. 180. /. 2.;
      let s = Js.Math.sin(fovy);
      let rd = 1. /. (far -. near);
      let ct = Js.Math.cos(fovy) /. s;

      let resultFloat32Arr = value(mat);

      Float32Array.unsafe_set(resultFloat32Arr, 0, ct /. aspect);
      Float32Array.unsafe_set(resultFloat32Arr, 1, 0.);
      Float32Array.unsafe_set(resultFloat32Arr, 2, 0.);
      Float32Array.unsafe_set(resultFloat32Arr, 3, 0.);
      Float32Array.unsafe_set(resultFloat32Arr, 4, 0.);
      Float32Array.unsafe_set(resultFloat32Arr, 5, ct);
      Float32Array.unsafe_set(resultFloat32Arr, 6, 0.);
      Float32Array.unsafe_set(resultFloat32Arr, 7, 0.);
      Float32Array.unsafe_set(resultFloat32Arr, 8, 0.);
      Float32Array.unsafe_set(resultFloat32Arr, 9, 0.);
      Float32Array.unsafe_set(resultFloat32Arr, 10, -. (far +. near) *. rd);
      Float32Array.unsafe_set(resultFloat32Arr, 11, -1.);
      Float32Array.unsafe_set(resultFloat32Arr, 12, 0.);
      Float32Array.unsafe_set(resultFloat32Arr, 13, 0.);
      Float32Array.unsafe_set(
        resultFloat32Arr,
        14,
        (-2.) *. far *. near *. rd,
      );
      Float32Array.unsafe_set(resultFloat32Arr, 15, 0.);

      resultFloat32Arr |> createWithoutCheck |> ResultContainerVO.succeed;
    };
};

let setTranslation = (v, mat) => {
  let resultFloat32Arr = value(mat);
  let (x, y, z) = VectorMathVO.value(v);

  Float32Array.unsafe_set(resultFloat32Arr, 12, x);
  Float32Array.unsafe_set(resultFloat32Arr, 13, y);
  Float32Array.unsafe_set(resultFloat32Arr, 14, z);

  resultFloat32Arr |> createWithoutCheck;
};

2)修改VectorContainerVO.re,实现值对象Matrix需要的函数
VectorContainerVO.re代码为:

let dot = (v1, v2) => {
  let (x, y, z) = value(v1);
  let (vx, vy, vz) = value(v2);

  x *. vx +. y *. vy +. z *. vz;
};

let sub = (v1, v2) => {
  let (x1, y1, z1) = value(v1);
  let (x2, y2, z2) = value(v2);

  create((x1 -. x2, y1 -. y2, z1 -. z2));
};

let scale = (scalar, v) => {
  let (x, y, z) = value(v);

  create((x *. scalar, y *. scalar, z *. scalar));
};

let cross = (v1, v2) => {
  let (x1, y1, z1) = value(v1);
  let (x2, y2, z2) = value(v2);

  create((y1 *. z2 -. y2 *. z1, z1 *. x2 -. z2 *. x1, x1 *. y2 -. x2 *. y1));
};

let normalize = v => {
  let (x, y, z) = value(v);

  let d = Js.Math.sqrt(x *. x +. y *. y +. z *. z);

  d === 0. ? create((0., 0., 0.)) : create((x /. d, y /. d, z /. d));
};

3)在src/domain_layer/domain/loop/render/service/中加入BuildRenderDataRenderDoService.re,创建领域服务BuildRenderData
BuildRenderDataRenderDoService.re代码为:

let build = gl => {
  SceneSceneGraphEntity.getCamera()
  |> OptionContainerDoService.get
  |> ResultContainerVO.bind(
       ({eye, center, up, near, far, fovy, aspect}: CameraSceneGraphVO.t) => {
       let vMatrix =
         MatrixMathVO.createIdentityMatrix()
         |> MatrixMathVO.setLookAt(eye, center, up);

       MatrixMathVO.createIdentityMatrix()
       |> MatrixMathVO.buildPerspective((fovy, aspect, near, far))
       |> ResultContainerVO.bind(pMatrix => {
            (vMatrix |> MatrixMathVO.value, pMatrix |> MatrixMathVO.value)
            //因为需要调用OptionContainerDoService.unsafeGet函数,从option数据中取出值(当option数据为None时,会抛出异常),所以需要使用Result.tryCatch将异常转换为Result
            |> ResultContainerVO.tryCatch(((vMatrix, pMatrix)) => {
                 (
                   gl,
                   {vMatrix, pMatrix}: RenderRenderVO.camera,
                   SceneSceneGraphEntity.getAllTriangles()
                   |> List.map(
                        (
                          {transform, geometry, material}: TriangleSceneGraphVO.t,
                        ) => {
                        let {position}: TransformSceneGraphVO.t = transform;
                        let {vbo, indices}: GeometrySceneGraphVO.t = geometry;
                        let {shader, colors}: MaterialSceneGraphVO.t = material;

                        let (vertexBuffer, indexBuffer) =
                          VBOManagerVBOManagerEntity.getVBOBuffers(
                            vbo |> OptionContainerDoService.unsafeGet,
                          );

                        (
                          {
                            mMatrix:
                              MatrixMathVO.createIdentityMatrix()
                              |> MatrixMathVO.setTranslation(
                                   PositionSceneGraphVO.value(position),
                                 )
                              |> MatrixMathVO.value,
                            vertexBuffer:
                              vertexBuffer
                              |> OptionContainerDoService.unsafeGet
                              |> VertexBufferVBOManagerVO.value,
                            indexBuffer:
                              indexBuffer
                              |> OptionContainerDoService.unsafeGet
                              |> IndexBufferVBOManagerVO.value,
                            indexCount: IndicesSceneGraphVO.length(indices),
                            colors:
                              colors |> List.map(Color3ContainerVO.value),
                            program:
                              ShaderManagerShaderEntity.getProgram(shader)
                              |> OptionContainerDoService.unsafeGet,
                          }: RenderRenderVO.triangle
                        );
                      }),
                 )
               })
          });
     });
};

4)修改SceneSceneGraphEntity.re和相关的仓库,实现getCamera函数
SceneSceneGraphEntity.re相关代码为:

let getCamera = () => {
  SceneRepo.getCamera();
};

SceneRepo.re相关代码为:

let _getCamera = ({camera}) => camera;

let getCamera = () => {
  _getCamera(Repo.getScene())
  |> OptionContainerDoService.map(CameraSceneRepo.build);
};

CamereSceneRepo.re相关代码为:

let build =
    ({eye, center, up, near, far, fovy, aspect}: ScenePOType.camera)
    : CameraSceneGraphVO.t => {
  {
    eye: eye |> VectorMathVO.create |> EyeSceneGraphVO.create,
    center: center |> VectorMathVO.create |> CenterSceneGraphVO.create,
    up: up |> VectorMathVO.create |> UpSceneGraphVO.create,
    near: NearSceneGraphVO.create(near),
    far: FarSceneGraphVO.create(far),
    fovy: FovySceneGraphVO.create(fovy),
    aspect: AspectSceneGraphVO.create(aspect),
  };
};

5)修改VBOManagerVBOManagerEntity.re和相关的仓库,实现getVBOBuffers函数
VBOManagerVBOManagerEntity.re相关代码为:

let getVBOBuffers = vbo => {
  VBOManagerRepo.getVBOBuffers(vbo);
};

VBOManagerRepo.re相关代码为:

let getVBOBuffers = vbo => {
  let vboId = VBOVBOManagerEntity.getId(vbo);

  (
    _getVertexBufferMap(Repo.getVBOManager())
    |> ImmutableSparseMap.get(vboId)
    |> OptionContainerDoService.map(VertexBufferVBOManagerVO.create),
    _getIndexBufferMap(Repo.getVBOManager())
    |> ImmutableSparseMap.get(vboId)
    |> OptionContainerDoService.map(IndexBufferVBOManagerVO.create),
  );
};

6)修改IndicesSceneGraphVO.re,实现length函数
IndicesSceneGraphVO.re相关代码:

let map = (f, indices) => indices |> value |> f;

let length = indices => indices |> map(Uint16Array.length);

7)修改ShaderManagerShaderEntity.re和相关的仓库,实现getProgram函数
ShaderManagerShaderEntity.re相关代码:

let getProgram = shader => {
  ShaderManagerRepo.getProgram(shader);
};

ShaderManagerRepo.re相关代码:

let getProgram = shader => {
  _getProgramMap(Repo.getShaderManager())
  |> ImmutableHashMap.get(ShaderShaderEntity.getId(shader));
};

3、修改RenderRenderDoService.re,用伪代码实现“渲染三个三角形”
RenderRenderDoService.re相关伪代码为:

let render = () => {
  ContextContextEntity.getGl()
  |> ResultContainerVO.bind(gl => {
       ...

       BuildRenderDataRenderDoService.build(gl)
       |> ResultContainerVO.bind(renderData => {
            renderData
            |> ResultContainerVO.tryCatch(
                 (
                   (gl, camera, triangles),
                 ) => {
                 triangles
                 |> List.iter(
                      (
                        {
                          mMatrix,
                          vertexBuffer,
                          indexBuffer,
                          indexCount,
                          colors,
                          program,
                        }: RenderRenderVO.triangle,
                      ) => {
                        渲染每个三角形
                    })
               })
          });
     });
};

4、实现渲染每个三角形

1)修改RenderRenderDoService.re,实现伪代码:“渲染每个三角形”

“渲染每个三角形”伪代码的实现跟最小3D程序的_render函数中“渲染一个三角形”的代码差不多。最小3D程序的_render函数->渲染第一个三角形的相关代码为:

  //省略了Utils.re的相关实现代码

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

我们参考最小3D程序,实现RenderRenderDoService.re:

let _sendAttributeData = (vertexBuffer, program, gl) => {
  let positionLocation =
    ContextContextEntity.getAttribLocation(program, "a_position", gl);

  positionLocation === (-1)
    ? Error.error({j|Failed to get the storage location of a_position|j}) : ();

  ContextContextEntity.bindBuffer(
    ContextContextEntity.getArrayBuffer(gl),
    vertexBuffer,
    gl,
  );

  ContextContextEntity.vertexAttribPointer(
    ~gl,
    ~location=positionLocation,
    ~size=3,
    (),
  );
  ContextContextEntity.enableVertexAttribArray(positionLocation, gl);
};

let _sendCameraUniformData = ((vMatrix, pMatrix), program, gl) => {
  let vMatrixLocation =
    ContextContextEntity.unsafeGetUniformLocation(program, "u_vMatrix", gl);
  let pMatrixLocation =
    ContextContextEntity.unsafeGetUniformLocation(program, "u_pMatrix", gl);

  ContextContextEntity.uniformMatrix4fv(
    ~location=vMatrixLocation,
    ~value=vMatrix,
    ~gl,
    (),
  );
  ContextContextEntity.uniformMatrix4fv(
    ~location=pMatrixLocation,
    ~value=pMatrix,
    ~gl,
    (),
  );
};

let _sendModelUniformData = ((mMatrix, colors), program, gl) => {
  let mMatrixLocation =
    ContextContextEntity.unsafeGetUniformLocation(program, "u_mMatrix", gl);

  colors
  |> List.iteri((index, (r, g, b)) => {
       let colorLocation =
         ContextContextEntity.unsafeGetUniformLocation(
           program,
           {j|u_color$index|j},
           gl,
         );

       ContextContextEntity.uniform3f(colorLocation, r, g, b, gl);
     });

  ContextContextEntity.uniformMatrix4fv(
    ~location=mMatrixLocation,
    ~value=mMatrix,
    ~gl,
    (),
  );
  (mMatrixLocation, false, mMatrix, gl);
};

let render = () => {
  ContextContextEntity.getGl()
  |> ResultContainerVO.bind(gl => {
       ...

       BuildRenderDataRenderDoService.build(gl)
       |> ResultContainerVO.bind(renderData => {
            renderData
            |> ResultContainerVO.tryCatch(
                 (
                   (gl, {vMatrix, pMatrix}: RenderRenderVO.camera, triangles),
                 ) => {
                 triangles
                 |> List.iter(
                      (
                        {
                          mMatrix,
                          vertexBuffer,
                          indexBuffer,
                          indexCount,
                          colors,
                          program,
                        }: RenderRenderVO.triangle,
                      ) => {
                      ContextContextEntity.useProgram(program, gl);

                      _sendAttributeData(vertexBuffer, program, gl);

                      _sendCameraUniformData(
                        (vMatrix, pMatrix),
                        program,
                        gl,
                      );

                      _sendModelUniformData((mMatrix, colors), program, gl);

                      ContextContextEntity.bindBuffer(
                        WebGL1.getElementArrayBuffer(gl),
                        indexBuffer,
                        gl,
                      );

                      ContextContextEntity.drawElements(
                        ~gl,
                        ~count=indexCount,
                        (),
                      );
                    })
               })
          });
     });
};

2)修改ContextContextEntity.re,实现相关函数
ContextContextEntity.re相关代码为:

let useProgram = (program, gl) => WebGL1.useProgram(program, gl);

...

let getAttribLocation = (program, name, gl) =>
  WebGL1.getAttribLocation(program, name, gl);

let getUniformLocation = (program, name, gl) =>
  WebGL1.getUniformLocation(program, name, gl);

//如果location不存在(为null),则抛出异常;否则获得location
let unsafeGetUniformLocation = (program, name, gl) =>
  getUniformLocation(program, name, gl) |> Js.Null.getExn;

let bindBuffer = (bufferTarget, buffer, gl) =>
  WebGL1.bindBuffer(bufferTarget, buffer, gl);

let vertexAttribPointer =
    (~gl, ~size, ~location, ~type_=WebGL1.getFloat(gl), ()) =>
  WebGL1.vertexAttribPointer(location, size, type_, false, 0, 0, gl);

let enableVertexAttribArray = (location, gl) =>
  WebGL1.enableVertexAttribArray(location, gl);

let getArrayBuffer = gl => gl |> WebGL1.getArrayBuffer;

let getElementArrayBuffer = gl => gl |> WebGL1.getElementArrayBuffer;

let uniform3f = (location, x, y, z, gl) =>
  WebGL1.uniform3f(location, x, y, z, gl);

let uniformMatrix4fv = (~gl, ~location, ~value, ~transpose=false, ()) =>
  WebGL1.uniformMatrix4fv(location, transpose, value, gl);

let drawElements =
    (
      ~gl,
      ~count,
      ~mode=WebGL1.getTriangles(gl),
      ~type_=WebGL1.getUnsignedShort(gl),
      ~offset=0,
      (),
    ) =>
  WebGL1.drawElements(mode, count, type_, offset, gl);

实现用户代码并运行测试

1、在项目根目录上执行webpack命令,更新wd.js文件

yarn webpack

2、实现index.html相关代码

index.html代码为:

  <script>
    ...
    wd.Director.start();
  </script>

3、运行测试

运行index.html页面,显示三个三角形,如下图所示:
此处输入图片的描述

更新后的领域视图

通过本文的实现后, 加入了下面的领域模型:

  • “容器”限界上下文
    • 加入的领域服务
      Array、Option
    • 加入的值对象
      Result、ImmutableHashMap、MutableHashMap、ImmutableSparseMap、MutableSparseMap

总结

恭喜你,经过长途跋涉,终于实现了从最小3D程序中提炼引擎!

本文成果

我们通过本文的实现,获得了下面的成果:
1、实现了用户代码index.html
2、提炼了引擎
3、更新了领域视图

本文不足之处

1、需要进行优化:
1)初始化Shader时,使用getProgramParameter、getProgramParameter来验证的时间开销大
2)渲染三角形时,只需要传递一次相机数据

下文概要

在下文中,我们会解决本文的不足之处,对引擎进行优化。

本文完整代码地址

Book-Extract-Engine Github Repo

原文地址:https://www.cnblogs.com/chaogex/p/12418292.html