目录javascript
你们好,本文使用领域驱动设计的方法,从新设计最小3D程序,识别出“用户”和“引擎”角色,给出各类设计的视图。html
从0开发3D引擎(九):实现最小的3D程序-“绘制三角形”java
从0开发3D引擎(十一):使用领域驱动设计,从最小3D程序中提炼引擎(第二部分)git
从0开发3D引擎(补充):介绍领域驱动设计github
上文得到了下面的成果:
一、最小3D程序
二、领域驱动设计的通用语言web
Book-Demo-Triangle Github Repo数据库
一、场景逻辑和WebGL API的调用逻辑混杂在一块儿
二、存在重复代码:
1)在_init函数的“初始化全部Shader”中有重复的模式
2)在_render中,渲染三个三角形的代码很是类似
3)Utils的sendModelUniformData1和sendModelUniformData2有重复的模式
三、_init传递给主循环的数据过于复杂编程
咱们根据上文的成果,进行下面的设计:
一、识别最小3D程序的用户逻辑和引擎逻辑
二、根据用户逻辑,给出用例图,用于设计API
三、设计分层架构,给出架构视图
四、进行领域驱动设计的战略设计
1)划分引擎子域和限界上下文
2)给出限界上下文映射图
五、进行领域驱动设计的战术设计
1)识别领域概念
2)创建领域模型,给出领域视图
六、设计数据,给出数据视图
七、根据用例图,设计分层架构的API层
八、根据API层的设计,设计分层架构的应用服务层
九、进行一些细节的设计:
1)使用Result处理错误
2)使用“Discriminated Union类型”来增强值对象的值类型约束
十、基本的优化canvas
持久化数据
由于咱们并无使用数据库,不须要离线存储,因此本文提到的持久化数据是指:从程序启动到程序结束时,将数据保存到内存中api
//定义聚合根Scene的PO的类型 type scene = { ... }; //定义PO的类型 type po = { scene }; “PO”的类型为po,“Scene PO”的类型为scene
module SceneEntity = { //定义聚合根Scene的DO的类型 type t = { ... }; }; “Scene DO”的类型为SceneEntity.t
这只是目前的选型,在后面的文章中咱们会修改它们。
TinyWonder
由于本系列开发的引擎的素材来自于Wonder.js,只有最小化的功能,因此叫TinyWonder
从顶层来看,包含三个部分的逻辑:建立场景、初始化、主循环
咱们依次识别它们的用户逻辑和引擎逻辑:
一、建立场景
用户逻辑
引擎逻辑
二、初始化
用户逻辑
引擎逻辑
三、主循环
用户逻辑
引擎逻辑
index.html
/* “User.”表示这是用户要实现的函数 “EngineJsAPI.”表示这是引擎提供的API函数 使用"xxx()"表明某个函数 */ //由用户实现 module User = { let prepareSceneData = () => { let (canvasId, ...) = ... ... (canvasId, ...) }; ... }; let (canvasId, ...) = User.prepareSceneData(); //保存某个场景数据到引擎中 EngineJsAPI.setXXXSceneData(canvasId, ...); EngineJsAPI.进行初始化(); EngineJsAPI.开启主循环();
初始化对应的通用语言为:
最小3D程序的_init函数负责初始化
如今依次分析初始化的每一个步骤对应的代码:
一、得到WebGL上下文
相关代码为:
let canvas = DomExtend.querySelector(DomExtend.document, "#webgl"); let gl = WebGL1.getWebGL1Context( canvas, { "alpha": true, "depth": true, "stencil": false, "antialias": true, "premultipliedAlpha": true, "preserveDrawingBuffer": false, }: WebGL1.contextConfigJsObj, );
用户逻辑
咱们能够先识别出下面的用户逻辑:
用户须要传入webgl上下文的配置项到引擎中。
咱们进行相关的思考:
引擎应该增长一个传入配置项的API吗?
配置项应该保存到引擎中吗?
考虑到:
因此引擎不须要增长API,也不须要保存配置项,而是在“进行初始化”的API中传入“配置项”,使用一次后即丢弃。
引擎逻辑
二、初始化全部Shader
相关代码为:
let program1 = gl |> WebGL1.createProgram |> Utils.initShader(GLSL.vs1, GLSL.fs1, gl); let program2 = gl |> WebGL1.createProgram |> Utils.initShader(GLSL.vs2, GLSL.fs2, gl);
用户逻辑
用户须要将两组GLSL传入引擎,而且把GLSL组与三角形关联起来。
咱们进行相关的思考:
如何使GLSL组与三角形关联?
咱们看下相关的通用语言:
三角形与Shader一一对应,而Shader又与GLSL组一一对应。
所以,咱们能够在三角形中增长数据:Shader名称(类型为string),从而使三角形经过Shader名称与GLSL组一一关联。
更新后的三角形通用语言为:
根据以上的分析,咱们识别出下面的用户逻辑:
引擎逻辑
咱们如今来思考如何解决下面的不足之处:
存在重复代码:
1)在_init函数的“初始化全部Shader”中有重复的模式
解决方案:
一、得到全部Shader的Shader名称和GLSL组集合
二、遍历这个集合:
1)建立Program
2)初始化Shader
这样的话,就只须要写一份“初始化每一个Shader”的代码了,消除了重复。
根据以上的分析,咱们识别出下面的引擎逻辑:
三、初始化场景
相关代码为:
let (vertices1, indices1) = Utils.createTriangleVertexData(); let (vertices2, indices2) = Utils.createTriangleVertexData(); let (vertices3, indices3) = Utils.createTriangleVertexData(); let (vertexBuffer1, indexBuffer1) = Utils.initVertexBuffers((vertices1, indices1), gl); let (vertexBuffer2, indexBuffer2) = Utils.initVertexBuffers((vertices2, indices2), gl); let (vertexBuffer3, indexBuffer3) = Utils.initVertexBuffers((vertices3, indices3), gl); let (position1, position2, position3) = ( (0.75, 0., 0.), ((-0.), 0., 0.5), ((-0.5), 0., (-2.)), ); let (color1, (color2_1, color2_2), color3) = ( (1., 0., 0.), ((0., 0.8, 0.), (0., 0.5, 0.)), (0., 0., 1.), ); let ((eyeX, eyeY, eyeZ), (centerX, centerY, centerZ), (upX, upY, upZ)) = ( (0., 0.0, 5.), (0., 0., (-100.)), (0., 1., 0.), ); let (near, far, fovy, aspect) = ( 1., 100., 30., (canvas##width |> Js.Int.toFloat) /. (canvas##height |> Js.Int.toFloat), );
用户逻辑
引擎逻辑
主循环对应的通用语言为:
对应最小3D程序的_loop函数对应主循环,如今依次分析主循环的每一个步骤对应的代码:
一、开启主循环
相关代码为:
let rec _loop = data => DomExtend.requestAnimationFrame((time: float) => { _loopBody(data); _loop(data) |> ignore; });
用户逻辑
无
引擎逻辑
如今进入_loopBody函数:
二、设置清空颜色缓冲时的颜色值
相关代码为:
let _clearColor = ((gl, sceneData) as data) => { WebGL1.clearColor(0., 0., 0., 1., gl); data; }; let _loopBody = data => { data |> ... |> _clearColor |> ... };
用户逻辑
引擎逻辑
三、清空画布
相关代码为:
let _clearCanvas = ((gl, sceneData) as data) => { WebGL1.clear( WebGL1.getColorBufferBit(gl) lor WebGL1.getDepthBufferBit(gl), gl, ); data; }; let _loopBody = data => { data |> ... |> _clearCanvas |> ... };
用户逻辑
无
引擎逻辑
四、渲染
相关代码为:
let _loopBody = data => { data |> ... |> _render; };
用户逻辑
无
引擎逻辑
如今进入_render函数,咱们来分析“渲染”的每一个步骤对应的代码:
1)设置WebGL状态
_render函数中的相关代码为:
WebGL1.enable(WebGL1.getDepthTest(gl), gl); WebGL1.enable(WebGL1.getCullFace(gl), gl); WebGL1.cullFace(WebGL1.getBack(gl), gl);
用户逻辑
引擎逻辑
2)计算view matrix和projection matrix
_render函数中的相关代码为:
let vMatrix = Matrix.createIdentityMatrix() |> Matrix.setLookAt( (eyeX, eyeY, eyeZ), (centerX, centerY, centerZ), (upX, upY, upZ), ); let pMatrix = Matrix.createIdentityMatrix() |> Matrix.buildPerspective((fovy, aspect, near, far));
用户逻辑
无
引擎逻辑
3)计算三个三角形的model matrix
_render函数中的相关代码为:
let mMatrix1 = Matrix.createIdentityMatrix() |> Matrix.setTranslation(position1); let mMatrix2 = Matrix.createIdentityMatrix() |> Matrix.setTranslation(position2); let mMatrix3 = Matrix.createIdentityMatrix() |> Matrix.setTranslation(position3);
用户逻辑
无
引擎逻辑
4)渲染第一个三角形
_render函数中的相关代码为:
WebGL1.useProgram(program1, gl); Utils.sendAttributeData(vertexBuffer1, program1, gl); Utils.sendCameraUniformData((vMatrix, pMatrix), program1, gl); Utils.sendModelUniformData1((mMatrix1, color1), program1, gl); WebGL1.bindBuffer(WebGL1.getElementArrayBuffer(gl), indexBuffer1, gl); WebGL1.drawElements( WebGL1.getTriangles(gl), indices1 |> Js.Typed_array.Uint16Array.length, WebGL1.getUnsignedShort(gl), 0, gl, );
用户逻辑
无
引擎逻辑
2)渲染第二个和第三个三角形
_render函数中的相关代码为:
WebGL1.useProgram(program2, gl); Utils.sendAttributeData(vertexBuffer2, program2, gl); Utils.sendCameraUniformData((vMatrix, pMatrix), program2, gl); Utils.sendModelUniformData2((mMatrix2, color2_1, color2_2), program2, gl); WebGL1.bindBuffer(WebGL1.getElementArrayBuffer(gl), indexBuffer2, gl); WebGL1.drawElements( WebGL1.getTriangles(gl), indices2 |> Js.Typed_array.Uint16Array.length, WebGL1.getUnsignedShort(gl), 0, gl, ); WebGL1.useProgram(program1, gl); Utils.sendAttributeData(vertexBuffer3, program1, gl); Utils.sendCameraUniformData((vMatrix, pMatrix), program1, gl); Utils.sendModelUniformData1((mMatrix3, color3), program1, gl); WebGL1.bindBuffer(WebGL1.getElementArrayBuffer(gl), indexBuffer3, gl); WebGL1.drawElements( WebGL1.getTriangles(gl), indices3 |> Js.Typed_array.Uint16Array.length, WebGL1.getUnsignedShort(gl), 0, gl, );
用户逻辑
与“渲染第一个三角形”的用户逻辑同样,只是将第一个三角形的数据换成第二个和第三个三角形的数据
引擎逻辑
与“渲染第一个三角形”的引擎逻辑同样,只是将第一个三角形的数据换成第二个和第三个三角形的数据
识别出两个角色:
咱们把用户逻辑中须要用户实现的逻辑移到角色“index.html”中;
把用户逻辑中须要调用API实现的逻辑做为用例,移到角色“引擎”中。
获得的用例图以下所示:
咱们使用四层的分层架构,架构视图以下所示:
不容许跨层访问。
对于“API层”和“应用服务层”,咱们会在给出领域视图后,详细设计它们。
咱们加入了“仓库”,使“实体”只能经过“仓库”来操做“数据”,隔离“数据”和“实体”。
只有“实体”负责持久化数据,因此只有“实体”依赖“仓库”,“值对象”和“领域服务”都不该该依赖“仓库”。
之因此“仓库”依赖了“领域服务”、“实体”、“值对象”,是由于“仓库”须要调用它们的函数,实现“数据”的PO和领域层的DO之间的转换。
对于“仓库”、“数据”、PO、DO,咱们会在后面的“设计数据”中详细分析。
“外部”负责与引擎的外部交互。
它包含两个部分:
以下图所示:
以下图所示:
其中:
上下文关系的介绍详见上下文映射图
如今咱们来分析下防腐层(ACL)的设计,其中相关的领域模型会在后面的“领域视图”中给出。
一、“着色器”限界上下文提供着色器的DO数据
二、“初始化全部Shader”限界上下文的领域服务BuildInitShaderData做为防腐层,将着色器DO数据转换为值对象InitShader
三、“初始化全部Shader”限界上下文的领域服务InitShader遍历值对象InitShader,初始化每一个Shader
经过这样的设计,隔离了领域服务InitShader和“着色器”限界上下文。
根据识别的引擎逻辑,能够得知值对象InitShader的值是全部Shader的Shader名称和GLSL组集合,所以咱们能够给出值对象InitShader的类型定义:
type singleInitShader = { shaderId: string, vs: string, fs: string, }; //值对象InitShader类型定义 type initShader = list(singleInitShader);
一、“场景图”限界上下文提供场景图的DO数据
二、“渲染”限界上下文的领域服务BuildRenderData做为防腐层,将场景图DO数据转换为值对象Render
三、“渲染”限界上下文的领域服务Render遍历值对象Render,渲染场景中每一个三角形
经过这样的设计,隔离了领域服务Render和“场景图”限界上下文。
最小3D程序的_render函数的参数是渲染须要的数据,这里称之为“渲染数据”。
最小3D程序的_render函数的参数以下:
let _render = ( ( gl, ( (program1, program2), (indices1, indices2, indices3), (vertexBuffer1, indexBuffer1), (vertexBuffer2, indexBuffer2), (vertexBuffer3, indexBuffer3), (position1, position2, position3), (color1, (color2_1, color2_2), color3), ( ( (eyeX, eyeY, eyeZ), (centerX, centerY, centerZ), (upX, upY, upZ), ), (near, far, fovy, aspect), ), ), ), ) => { ... };
如今,咱们结合识别的引擎逻辑,对渲染数据进行抽象,提炼出值对象Render,并给出值对象Render的类型定义。
由于渲染数据包含三个部分的数据:WebGL的上下文gl、场景中惟一的相机数据、场景中全部三角形的数据,因此值对象Render也应该包含这三个部分的数据:WebGL的上下文gl、相机数据、三角形数据
能够直接把渲染数据中的WebGL的上下文gl放到值对象Render中
对于渲染数据中的“场景中惟一的相机数据”:
( ( (eyeX, eyeY, eyeZ), (centerX, centerY, centerZ), (upX, upY, upZ), ), (near, far, fovy, aspect), ),
根据识别的引擎逻辑,咱们知道在渲染场景中全部的三角形前,须要根据这些渲染数据计算一个view matrix和一个projection matrix。由于值对象Render是为渲染全部三角形服务的,因此值对象Render的相机数据应该为一个view matrix和一个projection matrix
对于下面的渲染数据:
(position1, position2, position3),
根据识别的引擎逻辑,咱们知道在渲染场景中全部的三角形前,须要根据这些渲染数据计算每一个三角形的model matrix,因此值对象Render的三角形数据应该包含每一个三角形的model matrix
对于下面的渲染数据:
(indices1, indices2, indices3),
根据识别的引擎逻辑,咱们知道在调用drawElements绘制每一个三角形时,须要根据这些渲染数据计算顶点个数,做为drawElements的第二个形参,因此值对象Render的三角形数据应该包含每一个三角形的顶点个数
对于下面的渲染数据:
(program1, program2), (vertexBuffer1, indexBuffer1), (vertexBuffer2, indexBuffer2), (vertexBuffer3, indexBuffer3),
它们能够做为值对象Render的三角形数据。通过抽象后,值对象Render的三角形数据应该包含每一个三角形关联的program、每一个三角形的VBO数据(一个vertex buffer和一个index buffer)
对于下面的渲染数据(三个三角形的颜色数据),咱们须要从中设计出值对象Render的三角形数据包含的颜色数据:
(color1, (color2_1, color2_2), color3),
咱们须要将其统一为一个数据结构,才能做为值对象Render的颜色数据。
咱们回顾下将会在本文解决的不足之处:
二、存在重复代码:
...
2)在_render中,渲染三个三角形的代码很是类似
3)Utils的sendModelUniformData1和sendModelUniformData2有重复的模式
这两处的重复跟颜色的数据结构不统一是有关系的。
咱们来看下最小3D程序中相关的代码:
Main.re
let _render = (...) => { ... //渲染第一个三角形 ... Utils.sendModelUniformData1((mMatrix1, color1), program1, gl); ... //渲染第二个三角形 ... Utils.sendModelUniformData2((mMatrix2, color2_1, color2_2), program2, gl); ... //渲染第三个三角形 ... Utils.sendModelUniformData1((mMatrix3, color3), program1, gl); ... };
Utils.re
let sendModelUniformData1 = ((mMatrix, color), program, gl) => { ... let colorLocation = _unsafeGetUniformLocation(program, "u_color0", gl); ... _sendColorData(color, gl, colorLocation); }; let sendModelUniformData2 = ((mMatrix, color1, color2), program, gl) => { ... let color1Location = _unsafeGetUniformLocation(program, "u_color0", gl); let color2Location = _unsafeGetUniformLocation(program, "u_color1", gl); ... _sendColorData(color1, gl, color1Location); _sendColorData(color2, gl, color2Location); };
经过仔细分析这些相关的代码,咱们能够发现这两处的重复其实都由同一个缘由形成的:
因为第一个和第三个三角形的颜色数据与第二个三角形的颜色数据不一样,须要调用对应的sendModelUniformData1或sendModelUniformData2方法来传递对应三角形的颜色数据。
解决“Utils的sendModelUniformData1和sendModelUniformData2有重复的模式”
那是否能够把全部三角形的颜色数据统一用一个数据结构来保存,而后在渲染三角形->传递三角形的颜色数据时,遍历该数据结构,只用一个函数(而不是两个函数:sendModelUniformData一、sendModelUniformData2)传递对应的颜色数据,从而解决该重复呢?
咱们来分析下三个三角形的颜色数据:
第一个和第三个三角形只有一个颜色数据,类型为(float, float, float);
第二个三角形有两个颜色数据,它们的类型也为(float, float, float)。
根据分析,咱们做出下面的设计:
可使用列表来保存一个三角形全部的颜色数据,它的类型为list((float,float,float));
在传递该三角形的颜色数据时,遍历列表,传递每一个颜色数据。
相关伪代码以下:
let sendModelUniformData = ((mMatrix, colors: list((float,float,float))), program, gl) => { colors |> List.iteri((index, (r, g, b)) => { let colorLocation = _unsafeGetUniformLocation(program, {j|u_color$index|j}, gl); WebGL1.uniform3f(colorLocation, r, g, b, gl); }); ... };
这样咱们就解决了该重复。
解决“在_render中,渲染三个三角形的代码很是类似”
经过“统一用一种数据结构来保存颜色数据”,就能够构造出值对象Render,从而解决该重复了:
咱们再也不须要写三段代码来渲染三个三角形了,而是只写一段“渲染每一个三角形”的代码,而后在遍历值对象Render时执行它。
相关伪代码以下:
let 渲染每一个三角形 = (每一个三角形的数据) => {...}; let _render = (...) => { ... 构造值对象Render(场景图数据) |> 遍历值对象Render的三角形数据((每一个三角形的数据) => { 渲染每一个三角形(每一个三角形的数据) }); ... };
经过前面对渲染数据的分析,能够给出值对象Render的类型定义:
type triangle = { mMatrix: Js.Typed_array.Float32Array.t, vertexBuffer: WebGL1.buffer, indexBuffer: WebGL1.buffer, indexCount: int, //使用统一的数据结构 colors: list((float, float, float)), program: WebGL1.program, }; type triangles = list(triangle); type camera = { vMatrix: Js.Typed_array.Float32Array.t, pMatrix: Js.Typed_array.Float32Array.t, }; type gl = WebGL1.webgl1Context; //值对象Render类型定义 type render = (gl, camera, triangles);
识别出新的领域概念:
领域视图以下所示,图中包含了领域模型之间的全部聚合、组合关系,以及领域模型之间的主要依赖关系
以下图所示:
PO Container做为一个容器,负责保存PO到内存中。
PO Container应该为一个全局Record,有一个可变字段po,用于保存PO
相关的设计为:
type poContainer = { mutable po }; let poContainer = { po: 建立PO() };
这里有两个坏味道:
咱们应该尽可能使用局部变量和不可变数据/不可变操做,消除共享的状态。但有时候坏味道不可避免,所以咱们使用下面的策略来处理坏味道:
咱们设计以下:
相关的设计为:
type po = { //各个聚合根的数据 canvas, shaderManager, scene, context, vboManager };
由于如今信息不够,因此不设计聚合根的具体数据,留到实现时再设计它们。
容器管理负责读/写PO Container的PO,相关设计以下:
type getPO = unit => po; type setPO = po => unit;
module Repo = { //从PO中得到ShaderManager PO,转成ShaderManager DO,返回给领域层 type getShaderManager = unit => shaderManager; //转换来自领域层的ShaderManager DO为ShaderManager PO,设置到PO中 type setShaderManager = shaderManager => unit; type getCanvas = unit => canvas; type setCanvas = canvas => unit; type getScene = unit => scene; type setScene = scene => unit; type getVBOManager = unit => vboManager; type setVBOManager = vboManager => unit; type getContext = unit => context; type setContext = context => unit; }; module CreateRepo = { //建立各个聚合根的PO数据,如建立ShaderManager PO let create = () => { shaderManager: ..., ... }; }; module ShaderManagerRepo = { //从PO中得到ShaderManager PO的某个字段,转成DO,返回给领域层 type getXXX = po => xxx; //转换来自领域层的ShaderManager DO的某个字段为ShaderManager PO的对应字段,设置到PO中 type setXXX = (...) => unit; }; module CanvasRepo = { type getXXX = unit => xxx; type setXXX = (...) => unit; }; module SceneRepo = { type getXXX = unit => xxx; type setXXX = (...) => unit; }; module VBOManagerRepo = { type getXXX = unit => xxx; type setXXX = (...) => unit; }; module ContextRepo = { type getXXX = unit => xxx; type setXXX = (...) => unit; };
用户为index.html页面,它只知道javascript,不知道Reason
咱们根据用户的特色,决定设计原则:
首先根据用例图的用例,划分API模块;
而后根据API的设计原则,在对应模块中设计具体的API,给出API的类型签名。
API模块及其API的设计为:
module DirectorJsAPI = { //WebGL1.contextConfigJsObj是webgl上下文配置项的类型 type init = WebGL1.contextConfigJsObj => unit; type start = unit => unit; }; module CanvasJsAPI = { type canvasId = string; type setCanvasById = canvasId => unit; }; module ShaderJsAPI = { type shaderName = string; type vs = string; type fs = string; type addGLSL = (shaderName, (vs, fs)) => unit; }; module SceneJsAPI = { type vertices = Js.Typed_array.Float32Array.t; type indices = Js.Typed_array.Uint16Array.t; type createTriangleVertexData = unit => (vertices, indices); //由于“传入一个三角形的位置数据”、“传入一个三角形的顶点数据”、“传入一个三角形的Shader名称”、“传入一个三角形的颜色数据”都属于传入三角形的数据,因此应该只用一个API接收三角形的这些数据,这些数据应该分红三部分:Transform数据、Geometry数据和Material数据。API负责在场景中加入一个三角形。 type position = (float, float, float); type vertices = Js.Typed_array.Float32Array.t; type indices = Js.Typed_array.Uint16Array.t; type shaderName = string; type color3 = (float, float, float); type addTriangle = (position, (vertices, indices), (shaderName, array(color3))) => unit; type eye = (float, float, float); type center = (float, float, float); type up = (float, float, float); type viewMatrixData = (eye, center, up); type near = float; type far = float; type fovy = float; type aspect = float; type projectionMatrixData = (near, far, fovy, aspect); //函数名为“set”而不是“add”的缘由是:场景中只有一个相机,所以不须要加入操做,只须要设置惟一的相机 type setCamera = (viewMatrixData, projectionMatrixData) => unit; }; module GraphicsJsAPI = { type color4 = (float, float, float, float); type setClearColor = color4 => unit; };
咱们进行下面的设计:
目前来看,VO与DTO基本相同。
应用服务模块及其函数设计为:
module DirectorApService = { type init = WebGL1.contextConfigJsObj => unit; type start = unit => unit; }; module CanvasApService = { type canvasId = string; type setCanvasById = canvasId => unit; }; module ShaderApService = { type shaderName = string; type vs = string; type fs = string; type addGLSL = (shaderName, (vs, fs)) => unit; }; module SceneApService = { type vertices = Js.Typed_array.Float32Array.t; type indices = Js.Typed_array.Uint16Array.t; type createTriangleVertexData = unit => (vertices, indices); type position = (float, float, float); type vertices = Js.Typed_array.Float32Array.t; type indices = Js.Typed_array.Uint16Array.t; type shaderName = string; type color3 = (float, float, float); //注意:DTO(这个函数的参数)与VO(Scene API的addTriangle函数的参数)有区别:VO的颜色数据类型为array(color3),而DTO的颜色数据类型为list(color3) type addTriangle = (position, (vertices, indices), (shaderName, list(color3))) => unit; type eye = (float, float, float); type center = (float, float, float); type up = (float, float, float); type viewMatrixData = (eye, center, up); type near = float; type far = float; type fovy = float; type aspect = float; type projectionMatrixData = (near, far, fovy, aspect); type setCamera = (viewMatrixData, projectionMatrixData) => unit; }; module GraphicsApService = { type color4 = (float, float, float, float); type setClearColor = color4 => unit; };
咱们在从0开发3D引擎(五):函数式编程及其在引擎中的应用中介绍了“使用Result来处理错误”,它相比“抛出异常”的错误处理方式,有不少优势。
咱们在引擎中主要使用Result来处理错误。可是在后面的“优化”中,咱们能够看到为了优化,引擎也使用了“抛出异常”的错误处理方式。
咱们以值对象Matrix为例,来看下如何增强值对象的值类型约束,从而在编译检查时确保类型正确:
Matrix的值类型为Js.Typed_array.Float32Array.t,这样的类型设计有个缺点:不能与其它Js.Typed_array.Float32Array.t类型的变量区分开。
所以,在Matrix中可使用Discriminated Union类型来定义“Matrix”类型:
type t = | Matrix(Js.Typed_array.Float32Array.t);
这样就能解决该缺点了。
咱们在性能热点处进行下面的优化:
哪些地方属于性能热点呢?
咱们须要进行benchmark测试来肯定性能热点,不过通常来讲下面的场景属于性能热点的几率比较大:
具体来讲,目前引擎的适用于此处提出的优化的性能热点为:
let 初始化全部Shader = (...) => { ... //着色器数据中有“Discriminated Union”类型的数据,而构造后的值对象InitShader的值均为primitive类型 构造为值对象InitShader(着色器数据) |> //使用Result.tryCatch将异常转换为Result Result.tryCatch((值对象InitShader) => { //使用“抛出异常”的方式处理错误 根据值对象InitShader,初始化每一个Shader }); //由于值对象InitShader是只读数据,因此不须要将值对象InitShader更新到着色器数据中 };
let 渲染 = (...) => { ... //场景图数据中有“Discriminated Union”类型的数据,而构造后的值对象Render的值均为primitive类型 构造值对象Render(场景图数据) |> //使用Result.tryCatch将异常转换为Result Result.tryCatch((值对象Render) => { //使用“抛出异常”的方式处理错误 根据值对象Render,渲染每一个三角形 }); //由于值对象Render是只读数据,因此不须要将值对象Render更新到场景图数据中 };
咱们经过本文的领域驱动设计,得到了下面的成果:
一、用户逻辑和引擎逻辑
二、分层架构视图和每一层的设计
三、领域驱动设计的战略成果
1)引擎子域和限界上下文划分
2)限界上下文映射图
四、领域驱动设计的战术成果
1)领域概念
2)领域视图
五、数据视图和PO的相关设计
六、一些细节的设计
七、基本的优化
本文解决了上文的不足之处:
一、场景逻辑和WebGL API的调用逻辑混杂在一块儿
本文识别出用户index.html和引擎这两个角色,分离了用户逻辑和引擎,从而解决了这个不足
二、存在重复代码:
1)在_init函数的“初始化全部Shader”中有重复的模式
2)在_render中,渲染三个三角形的代码很是类似
3)Utils的sendModelUniformData1和sendModelUniformData2有重复的模式
本文提出了值对象InitShader和值对象Render,分别用一份代码实现“初始化每一个Shader”和“渲染每一个三角形”,而后分别在遍历对应的值对象时调用对应的一份代码,从而消除了重复
三、_init传递给主循环的数据过于复杂
本文对数据进行了设计,将数据分为VO、DTO、DO、PO,从而再也不传递数据,解决了这个不足
一、仓库与领域模型之间存在循环依赖
二、没有隔离基础设施层的“数据”的变化对领域层的影响
如在支持多线程时,须要增长渲染线程的数据,则不该该影响支持单线程的相关代码
三、没有隔离“WebGL”的变化
如在支持WebGL2时,不该该影响支持WebGL1的代码
在下文中,咱们会根据本文的成果,具体实现从最小的3D程序中提炼引擎。