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

admin/2020-03-05/ 分类:科技/阅读:

目录

  • 上一篇博文
  • 下一篇博文
  • 本文流程
  • 回顾上文
  • 解释基本的操作
  • 开始实现
    • 准备
    • 建立代码的文件夹结构,约定模块文件的命名规则
      • 模块文件的命名原则
      • 一级和二级文件夹
      • api_layer的文件夹
      • application_layer的文件夹
      • domain_layer的文件夹
      • infrastructure_layer的文件夹
    • index.html实现调用引擎API
    • 用伪代码实现index.html
    • 实现“CanvasJsAPI.setCanvasById”
    • 实现“GraphicsJsAPI.setClearColor”
    • 实现“ShaderJsAPI.addGLSL”
    • 实现“SceneJsAPI.createTriangleVertexData”
    • 实现“SceneJsAPI.addTriangle”
    • 实现“SceneJsAPI.setCamera”

大家好,本文根据领域驱动设计的成果,开始实现从最小的3D程序中提炼引擎。

上一篇博文

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

下一篇博文

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

本文流程

我们根据上文的成果,按照下面的步骤开始实现从最小的3D程序中提炼引擎:
1、建立代码的文件夹结构
2、index.html实现调用引擎API
3、根据用例图和API层的设计,用伪代码实现index.html
4、按照index.html从上往下的API调用顺序,依次实现API:setCanvasById、setClearColor、addGLSL、createTriangleVertexData、addTriangle、setCamera

回顾上文

上文的领域驱动设计获得了下面的成果:
1、用户逻辑和引擎逻辑
2、分层架构视图和每一层的设计
3、领域驱动设计的战略成果
1)引擎子域和限界上下文划分
2)限界上下文映射图
3)流程图
4、领域驱动设计的战术成果
1)领域概念
2)领域视图
5、数据视图和PO的相关设计
6、一些细节的设计
7、基本的优化

解释基本的操作

  • 如何在浏览器上运行index.html
    1、在TinyWonder项目根目录上执行start命令:
yarn start

2、在浏览器地址中输入下面的url并回车,即可运行index.html页面

http://127.0.0.1:8080

开始实现

打开最小3D程序的TinyWonder项目,现在我们开始具体实现。

准备

我们要完全重写src/的内容,因此在项目根目录上新建mine/文件夹,将src/文件夹拷贝mine/中,并清空src/文件夹。

通过备份src/文件夹,我们能容易地调出最小3D程序的代码供我们参考。

建立代码的文件夹结构,约定模块文件的命名规则

模块文件的命名原则

  • 加上所属层级/模块的后缀名
    这是为了减少重名的几率
  • 尽量简洁
    因此应该让后缀名尽可能地短,只要几乎不会出现重名的情况,那么不仅可以省略一些层级/模块的后缀名,而且有些模块文件甚至完全不加后缀名

一级和二级文件夹

如下图所示:

这是按照分层架构来划分的文件夹:

  • 一级文件夹(xxx_layer/)对应每个层级
  • 二级文件夹(xxx_layer/的子文件夹)对应每层的对象

api_layer的文件夹

api_layer/api/放置API模块文件,如SceneJsAPI.re等

application_layer的文件夹

application_layer/service/放置应用服务模块文件,如SceneApService.re等

domain_layer的文件夹

domain_layer/domain/放置领域服务、实体和值对象的模块文件

domain_layer/repo/放置仓库的模块文件

domain/的子文件夹对应引擎的各个子域,如下图所示:

引擎子域文件夹的子文件夹对应该子域的限界上下文,如下图所示:

限界上下文文件夹的子文件均为entity/、value_object/、service/,分别放置实体、值对象和领域服务的模块文件。
部分截图如下图所示:

entity/、value_object/、service/文件夹的模块文件的命名规则分别为:

  • 实体 限界上下文 Entity.re
    如SceneSceneGraphEntity.re
  • 值对象 限界上下文 VO.re
    如TriangleSceneGraphVO.re
  • 领域服务 限界上下文 DoService.re
    如RenderRenderDoService.re

如果从这三个子文件夹的文件中提出公共代码的模块文件(如在后面,会从值对象ImmutableHashMap和值对象MutableHashMap中提出HashMap模块),则该模块文件的命名规则为:
模块名 限界上下文.re
(如将HashMap模块文件命名为HashMapContainer.re)

infrastructure_layer的文件夹

infrastructure_layer/data/的文件夹结构如下图所示:

ContainerManager.re负责实现“容器管理”

container/放置PO Container相关的模块文件

po/放置PO类型定义的文件

infrastructure_layer/external/文件夹结构如下图所示:

external_object/放置外部对象的FFI文件
library/放置js库的FFI文件

index.html实现调用引擎API

index.html需要引入引擎文件,调用它的API。

我们首先考虑的实现方案是:
与最小3D程序一样,index.html以ES6 module的方式引入要使用的引擎的每个模块文件(一个.re的引擎文件就是一个模块文件),调用暴露的API函数。
index.html的相关伪代码如下:

<script type="module"> import { setCanvasById } from "./lib/es6_global/src/api_layer/api/CameraJsAPI.js"; import { start } from "./lib/es6_global/src/api_layer/api/DirectorJsAPI.js"; window.onload = () => { ... setCanvasById(canvasId); ... start(); ... }; </script>

这个方案有下面的缺点:

  • 用户访问的权限过大
    用户可以访问非API的函数,如引擎的私有函数
  • 用户需要知道要调用的引擎API在引擎的哪个模块文件中,以及模块文件的路径,这样会增加用户的负担
    如用户需要知道setCanvasById在CameraJsAPI.js中,并且需要知道CameraJsAPI.js的路径
  • 浏览器需要支持ES6 module import

因此,我们使用下面的方案来实现,该方案可以解决上一个方案的缺点:

  • 把引擎所有的API模块放到一个命名空间中,让用户通过它来调用API
    用户只能访问到API,从而让引擎控制了用户访问权限;
    用户只需要知道命名空间和API模块的名字,减少了负担。
  • 使用webpack,将与引擎API相关的文件打包成一个文件,在index.html中引入该文件
    这样浏览器就不需要支持ES6 module import了

我们通过下面的步骤来实现该方案:
1、创建gulp任务
该任务会创建src/Index.re文件,它引用了引擎所有的API模块。
通过下面的子步骤来实现该任务:
1)在项目根目录上,加入gulpfile.js文件
2)在gulpfile.js中,加入gulp任务:generateIndex,该任务负责把引擎的所有API模块放到Index.re文件中
3)实现generateIndex任务
因为我们希望用Reason而不是用js来实现,而且考虑到该任务属于通用任务,多个Reason项目都需要它,所以我们通过下面的步骤来实现generateIndex任务:
a)创建新项目:TinyWonderGenerateIndex
b)进入新项目,用Reason代码来实现generateIndex任务的逻辑
其中src/Generate.re的generate函数是提供给用户(如TinyWonder项目的generateIndex任务)使用的API,它的代码如下:

let generate = ( globCwd: string, rootDir: string, sourceFileGlobArr: array(string), destDir: string, config, ) => { let excludeList = config##exclude |> Array.to_list; let replaceAPIModuleNameFunc = config##replaceAPIModuleNameFunc |> Js.Option.getWithDefault(moduleName => moduleName |> Js.String.replace("JsAPI", "") ); sourceFileGlobArr |> Array.to_list |> List.fold_left( (fileDataList, filePath) => { let fileName = Path.basename_ext(filePath, ".re"); [ syncWithConfig(Path.join([|rootDir, filePath|]), {"cwd": globCwd}) |> Array.to_list |> List.filter(filePath => excludeList |> List.filter(exclude => filePath |> Js.String.includes(exclude) ) |> List.length === 0 ) |> List.map(filePath => ( Path.basename_ext(filePath, ".re") |> replaceAPIModuleNameFunc, Path.basename_ext(filePath, ".re"), Fs.readFileAsUtf8Sync(filePath) |> _findPublicFunctionList, ) ), ...fileDataList, ]; }, [], ) |> List.flatten |> _buildContent |> _writeToIndexFile(destDir); };

该函数使用glob库遍历sourceFileGlobArr数组,将“globCwd rootDir sourceFileGlob”路径中的所有Reason文件的函数引入到destDir/Index.re的对应的模块中。
可在config.exclude中定义要排除的文件路径的数组,在config.replaceAPIModuleNameFunc函数中定义如何重命名模块名。

该项目的完整代码地址为:Tiny-Wonder-GenerateIndex

举个例子来说明用户如何使用generate函数:
假设用户工作在TinyWonder项目(项目根目录为/y/Github/TinyWonder/)上,创建了./src/api/AJsAPI.re,它的代码为:

let aFunc = v => 1;

它的模块名为“AJsAPI”。

用户还创建了./src/api/ddd/BJsAPI.re,它的代码为:

let bFunc = v => v * 2;

它的模块名为“BJsAPI”

用户可以在TinyWonder项目根目录上,用js代码调用Tiny-Wonder-GenerateIndex项目的generate函数:

//将路径为“/y/Github/TinyWonder/src/**/api/**/*.re”(排除包含“src/Config”的文件路径)的所有API模块引入到./src/Index.re中 generate("/", "/y/Github/TinyWonder/src", ["**/api/**/*.re"], "./src/", { exclude: ["src/Config"], //该函数用于去掉API模块名的“JsAPI”,如将“AJsAPI”重命名为“A” replaceAPIModuleNameFunc: (moduleName) => moduleName.replace("JsAPI", "") })

用户调用后,会在./src/中加入Index.re文件,它的代码如下:

module A = { let aFunc = AJsAPI.aFunc; }; module B = { let bFunc = BJsAPI.bFunc; };

现在我们清楚了如何使用generate函数,那么继续在TinyWonderGenerateIndex项目上完成剩余工作:
c)编译该项目的Reason代码为Commonjs模块规范的js文件
d)通过npm发布该项目
package.json需要定义main字段:

"main": "./lib/js/src/Generate.js",

e)回到TinyWonder项目
f)在TinyWonder项目的gulpfile.js->generateIndex任务中,引入TinyWonderGenerateIndex项目,调用它的generate函数
TinyWonder项目的相关代码为:
gulpfile.js

var gulp = require("gulp"); var path = require("path"); gulp.task("generateIndex", function (done) { var generate = require("tiny-wonder-generate-index"); var rootDir = path.join(process.cwd(), "src"), destDir = "./src/"; generate.generate("/", rootDir, ["**/api/**/*.re"], destDir, { exclude: [], replaceAPIModuleNameFunc: (moduleName) => moduleName.replace("JsAPI", "") }); done(); });

2、创建gulp任务后,我们需要引入webpack,它将Index.re关联的引擎文件打包成一个文件
1)在项目的根目录上,加入webpack.config.js文件:

const path = require('path'); const isProd = process.env.NODE_ENV === 'production'; module.exports = { entry: { "wd": "./lib/es6_global/src/Index.js" }, mode: isProd ? 'production' : 'development', output: { filename: '[name].js', path: path.join(__dirname, "dist"), library: 'wd', libraryTarget: 'umd' }, target: "web" };

2)在package.json中,加入script:

"scripts": { ... "webpack:dev": "NODE_ENV=development webpack --config webpack.config.js", "webpack": "gulp generateIndex && npm run webpack:dev" }

3、运行测试
1)在src/api_layer/api/中加入一个用于测试的API模块文件:TestJsAPI.re,定义两个API函数:

let aFunc = v => Js.log(v); let bFunc = v => Js.log(v * 2);

2)安装gulp后,在项目根目录上执行generateIndex任务,进行运行测试:

gulp generateIndex

可以看到src/中加入了Index.re文件,Index.re代码为:

module Test = { let aFunc = TestJsAPI.aFunc; let bFunc = TestJsAPI.bFunc; };

3)在安装webpack后,在项目根目录上执行下面命令,打包为dist/wd.js文件,其中命名空间为“wd”:

yarn webpack

4)index.html引入wd.js文件,调用引擎API

index.html的代码为:

 <script src="./dist/wd.js"></script> <script> var a = wd.Test.aFunc(1); var b = wd.Test.bFunc(2); </script>

5)在浏览器上运行index.html,打开控制台,可以看到打印了"1"和“4”

用伪代码实现index.html

我们根据用例图和设计的API来实现index.html:
index.html需要使用最小3D程序的数据,来实现用例图中“index.html”角色包含的用户逻辑;
index.html需要调用引擎API,来实现用例图中的用例。

index.html的伪代码实现如下所示:

<canvas id="webgl" width="400" height="400"> Please use a browser that supports "canvas" </canvas> <script> //准备canvas的id var canvasId = "webgl"; CanvasJsAPI.setCanvasById(canvasId); //准备清空颜色缓冲时的颜色值 var clearColor = [0.0, 0.0, 0.0, 1.0]; GraphicsJsAPI.setClearColor(clearColor); //准备两组GLSL var vs1 = ` precision mediump float; attribute vec3 a_position; uniform mat4 u_pMatrix; uniform mat4 u_vMatrix; uniform mat4 u_mMatrix; void main() { gl_Position = u_pMatrix * u_vMatrix * u_mMatrix * vec4(a_position, 1.0); } `; var fs1 = ` precision mediump float; uniform vec3 u_color0; void main(){ gl_FragColor = vec4(u_color0, 1.0); } `; var vs2 = ` precision mediump float; attribute vec3 a_position; uniform mat4 u_pMatrix; uniform mat4 u_vMatrix; uniform mat4 u_mMatrix; void main() { gl_Position = u_pMatrix * u_vMatrix * u_mMatrix * vec4(a_position, 1.0); } `; var fs2 = ` precision mediump float; uniform vec3 u_color0; uniform vec3 u_color1; void main(){ gl_FragColor = vec4(u_color0 * u_color1, 1.0); } `; //准备两个Shader的名称 var shaderName1 = "shader1"; var shaderName2 = "shader2"; ShaderJsAPI.addGLSL(shaderName1, [vs1, fs1]); ShaderJsAPI.addGLSL(shaderName2, [vs2, fs2]); //调用API,准备三个三角形的顶点数据 var [vertices1, indices1] = SceneJsAPI.createTriangleVertexData(); var [vertices2, indices2] = SceneJsAPI.createTriangleVertexData(); var [vertices3, indices3] = SceneJsAPI.createTriangleVertexData(); //准备三个三角形的位置数据 var [position1, position2, position3] = [ [0.75, 0.0, 0.0], [-0.0, 0.0, 0.5], [-0.5, 0.0, -2.0] ]; //准备三个三角形的颜色数据 var [colors1, colors2, colors3] = [ [[1.0, 0.0, 0.0]], [[0.0, 0.8, 0.0], [0.0, 0.5, 0.0]], [[0.0, 0.0, 1.0]] ]; SceneJsAPI.addTriangle(position1, [vertices1, indices1], [shaderName1, colors1]); SceneJsAPI.addTriangle(position2, [vertices2, indices2], [shaderName2, colors2]); SceneJsAPI.addTriangle(position3, [vertices3, indices3], [shaderName1, colors3]); //准备相机数据 var [eye, center, up] = [ [0.0, 0.0, 5.0], [0.0, 0.0, -100.0], [0.0, 1.0, 0.0] ]; var [near, far, fovy, aspect] = [ 1.0, 100.0, 30.0, canvas.width / canvas.height ]; SceneJsAPI.setCamera([eye, center, up], [near, far, fovy, aspect]); //准备webgl上下文的配置项 var contextConfig = { "alpha": true, "depth": true, "stencil": false, "antialias": true, "premultipliedAlpha": true, "preserveDrawingBuffer": false, }; DirectorJsAPI.init(contextConfig); DirectorJsAPI.start(); </script>

我们按照下面的步骤来具体实现index.html:
1、按照index.html从上往下的API调用顺序,确定要实现的API
2、按照引擎的层级,从上层的API开始,实现每一层的对应模块
3、实现index.html的相关代码
4、运行测试

现在我们按照顺序,确定要实现API->“CanvasJsAPI.setCanvasById” 。

现在来实现该API:

实现“CanvasJsAPI.setCanvasById”

1、在src/api_layer/api/中加入CanvasJsAPI.re,实现API
CanvasJsAPI.re代码为:

//因为应用服务CanvasApService的setCanvasById函数输入和输出的DTO与这里(API)的setCanvasById函数输入和输出的VO相同,所以不需要在两者之间转换 let setCanvasById = CanvasApService.setCanvasById;

2、在src/application_layer/service/中加入CanvasApService.re,实现应用服务
CanvasApService.re代码为:

let setCanvasById = canvasId => { CanvasCanvasEntity.setCanvasById(canvasId); //打印canvasId,用于运行测试 Js.log(canvasId); };

3、把最小3D程序的DomExtend.re放到src/instracture_layer/external/external_object/中,删除目前没用到的代码(删除requestAnimationFrame FFI)
DomExtend.re代码为:

type htmlElement = { . "width": int, "height": int, }; type body; type document = {. "body": body}; [@bs.val] external document: document = ""; [@bs.send] external querySelector: (document, string) => htmlElement = "";

4、在src/domain_layer/domain/canvas/canvas/entity/中加入CanvasCanvasEntity.re,创建聚合根Canvas
CanvasCanvasEntity.re代码为:

//这里定义Canvas DO的类型(Canvas DO为一个画布) type t = DomExtend.htmlElement; let setCanvasById = canvasId => Repo.setCanvas( DomExtend.querySelector(DomExtend.document, {j|#$canvasId|j}), );

5、因为需要使用Result来处理错误,所以加入值对象Result
1)在领域视图的“容器”限界上下文中,加入值对象Result,负责操作错误处理的容器Result
2)在src/domain_layer/domain/structure/container/value_object/中加入ResultContainerVO.re,创建值对象Result
ResultContainerVO.re代码为:

type t('a, 'b) = | Success('a) | Fail('b); let succeed = x => Success(x); let fail = x => Fail(x); let _raiseErrorAndReturn = msg => Js.Exn.raiseError(msg); let failWith = x => (x |> _raiseErrorAndReturn)->Fail; let either = (successFunc, failureFunc, twoTrackInput) => switch (twoTrackInput) { | Success(s) => successFunc(s) | Fail(f) => failureFunc(f) }; let bind = (switchFunc, twoTrackInput) => either(switchFunc, fail, twoTrackInput); let tap = (oneTrackFunc, twoTrackInput) => either( result => { result |> oneTrackFunc |> ignore; result |> succeed; }, fail, twoTrackInput, ); let tryCatch = (oneTrackFunc: 'a => 'b, x: 'a): t('b, Js.Exn.t) => try(oneTrackFunc(x) |> succeed) { | Js.Exn.Error(e) => fail(e) | err => {j|unknown error: $err|j} |> _raiseErrorAndReturn |> fail }; let mapSuccess = (mapFunc, result) => switch (result) { | Success(s) => mapFunc(s) |> succeed | Fail(f) => fail(f) }; let handleFail = (handleFailFunc: 'f => unit, result: t('s, 'f)): unit => switch (result) { | Success(s) => () | Fail(f) => handleFailFunc(f) };

6、在src/infrastructure_layer/data/po/中加入POType.re,定义PO的类型,目前PO只包含Canvas PO
POType.re代码为:

//因为在创建PO时,Canvas PO(一个画布)并不存在,所以它为option类型 type po = {canvas: option(CanvasPOType.canvas)};

7、在src/infrastructure_layer/data/po/中加入CanvasPOType.re,定义Canvas PO类型
CanvasPOType.re代码为:

type canvas = DomExtend.htmlElement;

8、因为定义了option类型,所以加入领域服务Option
1)在领域视图的“容器”限界上下文中,加入领域服务Option,负责操作Option容器
2)在src/domain_layer/domain/structure/container/service/中加入OptionContainerDoService.re,创建领域服务Option
OptionContainerDoService.re代码为:

//如果optionData为Some(v),返回v;否则抛出异常 let unsafeGet = optionData => optionData |> Js.Option.getExn; //通过使用Result,安全地取出optionData的值 let get = optionData => { switch (optionData) { | None => ResultContainerVO.failWith({|data not exist(get by getExn)|}) | Some(data) => ResultContainerVO.succeed(data) }; };

9、在src/domain_layer/repo/中加入Repo.re,实现仓库对Canvas PO的操作
Repo.re代码为:

let getCanvas = () => { let po = ContainerManager.getPO(); po.canvas; }; let setCanvas = canvas => { let po = ContainerManager.getPO(); {...po, canvas: Some(canvas)} |> ContainerManager.setPO; };

10、在src/infrastructure_layer/data/中加入ContainerManager.re,实现PO Container中PO的读写
ContainerManager.re代码为:

let getPO = () => { Container.poContainer.po; }; let setPO = po => { Container.poContainer.po = po; };

11、在src/infrastructure_layer/data/container/中加入ContainerType.re和Container.re,分别定义PO Container的类型和创建PO Container
ContainerType.re代码为:

type poContainer = {mutable po: POType.po};

Container.re代码为:

let poContainer: ContainerType.poContainer = { po: CreateRepo.create(), };

12、在src/domain_layer/repo/中加入CreateRepo.re,实现创建PO
CreateRepo.re代码为:

open POType; let create = () => {canvas: None};

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

yarn webpack

14、实现index.html相关代码

index.html代码为:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>use engine</title> </head> <body> <canvas id="webgl" width="400" height="400"> Please use a browser that supports "canvas" </canvas> <script src="./dist/wd.js"></script> <script> //准备canvas的id var canvasId = "webgl"; //调用API wd.Canvas.setCanvasById(canvasId); </script> </body> </html>

15、运行测试

运行index.html页面

打开控制台,可以看到打印了"webgl"

实现“GraphicsJsAPI.setClearColor”

1、在src/api_layer/api/中加入GraphicsJsAPI.re,实现API
GraphicsJsAPI.re代码为:

let setClearColor = GraphicsApService.setClearColor;

2、在src/application_layer/service/中加入GraphicsApService.re,实现应用服务
GraphicsApService.re代码为:

let setClearColor = clearColor => { ContextContextEntity.setClearColor(Color4ContainerVO.create(clearColor)); //用于运行测试 Js.log(clearColor); };

3、在src/domain_layer/domain/structure/container/value_object/中加入Color4ContainerVO.re,创建值对象Color4
Color4ContainerVO.re代码为:

type r = float; type g = float; type b = float; type a = float; type t = | Color4(r, g, b, a); let create = ((r, g, b, a)) => Color4(r, g, b, a); let value = color => switch (color) { | Color4(r, g, b, a) => (r, g, b, a) };

4、在src/domain_layer/domain/webgl_context/context/entity/中加入ContextContextEntity.re,创建聚合根Context
ContextContextEntity.re代码为:

let setClearColor = clearColor => { ContextRepo.setClearColor(clearColor); };

5、在src/infrastructure_layer/data/po/中加入ContextPOType.re,定义Context PO类型
ContextPOType.re代码为:

type context = {clearColor: (float, float, float, float)};

6、修改POType.re
POType.re相关代码为:

type po = { ... context: ContextPOType.context, };

7、在src/domain_layer/repo/中加入ContextRepo.re,实现仓库对Context PO的clearColor字段的操作
ContextRepo.re代码为:

let getClearColor = () => { Repo.getContext().clearColor; }; let setClearColor = clearColor => { Repo.setContext({ ...Repo.getContext(), clearColor: Color4ContainerVO.value(clearColor), }); };

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

let getContext = () => { let po = ContainerManager.getPO(); po.context; }; let setContext = context => { let po = ContainerManager.getPO(); {...po, context} |> ContainerManager.setPO; };

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

let create = () => { ... context: { clearColor: (0., 0., 0., 1.), }, };

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

yarn webpack

11、实现index.html相关代码

index.html代码为:

 <script> ... //准备清空颜色缓冲时的颜色值 var clearColor = [0.0, 0.0, 0.0, 1.0]; wd.Graphics.setClearColor(clearColor); </script>

12、运行测试

运行index.html页面

打开控制台,可以看到打印了数组:[0,0,0,1]

实现“ShaderJsAPI.addGLSL”

1、在src/api_layer/api/中加入ShaderJsAPI.re,实现API
ShaderJsAPI.re代码为:

let addGLSL = ShaderApService.addGLSL;

2、设计领域模型ShaderManager、Shader、GLSL的DO

根据领域模型:

和识别的引擎逻辑:
获得所有Shader的Shader名称和GLSL组集合

我们可以设计聚合根ShaderManager的DO为集合list:

type t = {glsls: list(Shader DO)};

设计值对象GLSL的DO为:

type t = | GLSL(string, string);

设计实体Shader的DO为:

type shaderName = string; type t = | Shader(shaderName, GLSL DO);

3、在src/application_layer/service/中加入ShaderApService.re,实现应用服务
ShaderApService.re代码为:

let addGLSL = (shaderName, glsl) => { ShaderManagerShaderEntity.addGLSL( ShaderShaderEntity.create(shaderName, GLSLShaderVO.create(glsl)), ); //用于运行测试 Js.log((shaderName, glsl)); };

4、在src/domain_layer/domain/shader/shader/entity/中加入ShaderShaderEntity.re,创建实体Shader
ShaderShaderEntity.re代码为:

type shaderName = string; type t = | Shader(shaderName, GLSLShaderVO.t); let create = (shaderName, glsl) => Shader(shaderName, glsl); let getShaderName = shader => switch (shader) { | Shader(shaderName, glsl) => shaderName }; let getGLSL = shader => switch (shader) { | Shader(shaderName, glsl) => glsl };

5、在src/domain_layer/domain/shader/shader/value_object/中加入GLSLShaderVO.re,创建值对象GLSL
GLSLShaderVO.re代码为:

type t = | GLSL(string, string); let create = ((vs, fs)) => GLSL(vs, fs); let value = glsl => switch (glsl) { | GLSL(vs, fs) => (vs, fs) };

6、在src/domain_layer/domain/shader/shader/entity/中加入ShaderManagerShaderEntity.re,创建聚合根ShaderManager
ShaderManagerShaderEntity.re代码为:

type t = {glsls: list(ShaderShaderEntity.t)}; let addGLSL = shader => { ShaderManagerRepo.addGLSL(shader); };

7、在src/infrastructure_layer/data/po/中加入ShaderManagerPOType.re,定义ShaderManager PO的类型
ShaderManagerPOType.re代码为:

//shaderId就是Shader的名称 type shaderId = string; type shaderManager = {glsls: list((shaderId, (string, string)))};

8、修改POType.re
POType.re相关代码为:

type po = { ... shaderManager: ShaderManagerPOType.shaderManager, };

9、在src/domain_layer/repo/中加入ShaderManagerRepo.re,实现仓库对ShaderManager PO的glsls字段的操作
ShaderManagerRepo.re代码为:

open ShaderManagerPOType; let _getGLSLs = ({glsls}) => glsls; let addGLSL = shader => { Repo.setShaderManager({ ...Repo.getShaderManager(), glsls: [ ( ShaderShaderEntity.getShaderName(shader), shader |> ShaderShaderEntity.getGLSL |> GLSLShaderVO.value, ), ..._getGLSLs(Repo.getShaderManager()), ], }); };

10、修改Repo.re,实现仓库对ShaderManager PO的操作
Repo.re相关代码为:

let getShaderManager = () => { let po = ContainerManager.getPO(); po.shaderManager; }; let setShaderManager = shaderManager => { let po = ContainerManager.getPO(); {...po, shaderManager} |> ContainerManager.setPO; };

11、修改CreateRepo.re,实现创建ShaderManager PO
CreateRepo.re相关代码为:

let create = () => { ... shaderManager: { glsls: [], }, };

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

yarn webpack

13、实现index.html相关代码

index.html代码为:

 <script> ... //准备两组GLSL var vs1 = ` precision mediump float; attribute vec3 a_position; uniform mat4 u_pMatrix; uniform mat4 u_vMatrix; uniform mat4 u_mMatrix; void main() { gl_Position = u_pMatrix * u_vMatrix * u_mMatrix * vec4(a_position, 1.0); } `; var fs1 = ` precision mediump float; uniform vec3 u_color0; void main(){ gl_FragColor = vec4(u_color0, 1.0); } `; var vs2 = ` precision mediump float; attribute vec3 a_position; uniform mat4 u_pMatrix; uniform mat4 u_vMatrix; uniform mat4 u_mMatrix; void main() { gl_Position = u_pMatrix * u_vMatrix * u_mMatrix * vec4(a_position, 1.0); } `; var fs2 = ` precision mediump float; uniform vec3 u_color0; uniform vec3 u_color1; void main(){ gl_FragColor = vec4(u_color0 * u_color1, 1.0); } `; //准备两个Shader的名称 var shaderName1 = "shader1"; var shaderName2 = "shader2"; wd.Shader.addGLSL(shaderName1, [vs1, fs1]); wd.Shader.addGLSL(shaderName2, [vs2, fs2]); </script>

14、运行测试

运行index.html页面

打开控制台,可以看到打印了两个Shader的数据:

实现“SceneJsAPI.createTriangleVertexData”

1、在src/api_layer/api/中加入SceneJsAPI.re,实现API
SceneJsAPI.re代码为:

let createTriangleVertexData = SceneApService.createTriangleVertexData;

2、在src/application_layer/service/中加入SceneApService.re,实现应用服务
SceneApService.re代码为:

let createTriangleVertexData = () => { //vertices和indices为DO数据,分别为值对象Vertices的DO和值对象Indices的DO let (vertices, indices) = GeometrySceneGraphVO.createTriangleVertexData(); //将DO转成DTO let data = ( vertices |> VerticesSceneGraphVO.value, indices |> IndicesSceneGraphVO.value, ); //用于运行测试 Js.log(data); //将DTO返回给API层 data; };

3、在src/domain_layer/domain/scene/scene_graph/value_object/中加入GeometrySceneGraphVO.re,创建值对象Geometry
GeometrySceneGraphVO.re代码为:

let createTriangleVertexData = () => { open Js.Typed_array; let vertices = Float32Array.make([|0., 0.5, 0.0, (-0.5), (-0.5), 0.0, 0.5, (-0.5), 0.0|]) |> VerticesSceneGraphVO.create; let indices = Uint16Array.make([|0, 1, 2|]) |> IndicesSceneGraphVO.create; (vertices, indices); };

4、在src/domain_layer/domain/scene/scene_graph/value_object/中加入VerticesSceneGraphVO.re和IndicesSceneGraphVO.re,创建值对象Vertices和值对象Indices
VerticesSceneGraphVO.re代码为:

open Js.Typed_array; type t = | Vertices(Float32Array.t); let create = value => Vertices(value); let value = vertices => switch (vertices) { | Vertices(value) => value };

IndicesSceneGraphVO.re代码为:

open Js.Typed_array; type t = | Indices(Uint16Array.t); let create = value => Indices(value); let value = indices => switch (indices) { | Indices(value) => value };

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

yarn webpack

6、实现index.html相关代码

index.html代码为:

 <script> ... //调用API,准备三个三角形的顶点数据 var [vertices1, indices1] = wd.Scene.createTriangleVertexData(); var [vertices2, indices2] = wd.Scene.createTriangleVertexData(); var [vertices3, indices3] = wd.Scene.createTriangleVertexData(); </script>

7、运行测试

运行index.html页面

打开控制台,可以看到打印了三次顶点数据

实现“SceneJsAPI.addTriangle”

1、修改SceneJsAPI.re,实现API
SceneJsAPI.re相关代码为:

let addTriangle = (position, (vertices, indices), (shaderName, colors)) => { //这里的VO与DTO有区别:VO的colors的类型为array,而DTO的colors的类型为list,所以需要将colors的array转换为list SceneApService.addTriangle( position, (vertices, indices), (shaderName, colors |> Array.to_list), ); };

2、设计聚合根Scene、值对象Triangle和它所有的值对象的DO

根据领域模型:

我们按照Scene的聚合关系,从下往上开始设计:
设计值对象Vector的DO为:

type t = | Vector(float, float, float);

设计值对象Position的DO为:

type t = | Position(Vector.t);

设计值对象Vertices的DO为:

open Js.Typed_array; type t = | Vertices(Float32Array.t);

设计值对象Indices的DO为:

open Js.Typed_array; type t = | Indices(Uint16Array.t);

设计值对象Color3的DO为:

type r = float; type g = float; type b = float; type t = | Color3(r, g, b);

设计值对象Transform的DO为:

type t = {position: Position DO};

设计值对象Geometry的DO为:

type t = { vertices: Vertices DO, indices: Indices DO, };

对于值对象Material的DO,我们需要思考:
在领域模型中,Material组合了一个Shader,这应该如何体现到Material的DO中?

解决方案:
1)将Shader DO的Shader名称和值对象GLSL拆开
2)Shader DO只包含Shader名称,它即为实体Shader的id
3)Material DO包含一个Shader的id

这样就使Material通过Shader的id(Shader名称),与Shader关联起来了!

因为Shader DO移除了值对象GLSL,所以我们需要重写与Shader相关的代码:
1)重写ShaderShaderEntity.re
ShaderShaderEntity.re代码为:

type shaderName = string; type id = shaderName; type t = | Shader(id); let create = id => Shader(id); let getId = shader => switch (shader) { | Shader(id) => id };

2)重写ShaderManagerShaderEntity.re
ShaderManagerShaderEntity.re代码为:

type t = {glsls: list((ShaderShaderEntity.t, GLSLShaderVO.t))}; let addGLSL = (shader, glsl) => { ShaderManagerRepo.addGLSL(shader, glsl); };

3)重写ShaderApService.re
ShaderApService.re代码为:

let addGLSL = (shaderName, glsl) => { ShaderManagerShaderEntity.addGLSL( ShaderShaderEntity.create(shaderName), GLSLShaderVO.create(glsl), ); //用于运行测试 Js.log((shaderName, glsl)); };

4)重写ShaderManagerRepo.re
ShaderManagerRepo.re代码为:

open ShaderManagerPOType; let _getGLSLs = ({glsls}) => glsls; let addGLSL = (shader, glsl) => { Repo.setShaderManager({ ...Repo.getShaderManager(), glsls: [ (ShaderShaderEntity.getId(shader), GLSLShaderVO.value(glsl)), ..._getGLSLs(Repo.getShaderManager()), ], }); };

现在我们可以设计值对象Material的DO为:

type t = { shader: Shader DO, colors: list(Color3 DO), };

注意:这里的字段名是“shader”而不是“shaderName”或者“shaderId”,因为这样才能直接体现Material组合了一个Shader,而不是组合了一个Shader名称或Shader id

我们继续设计,设计值对象Triangle的DO为:

type t = { transform: Transform DO, geometry: Geometry DO, material: Material DO, };

设计聚合根Scene的DO为:

type t = {triangles: list(Triangle DO)};

3、修改SceneApService.re,实现应用服务
SceneApService.re相关代码为:

let addTriangle = (position, (vertices, indices), (shaderName, colors)) => { SceneSceneGraphEntity.addTriangle( position |> VectorContainerVO.create |> PositionSceneGraphVO.create, ( VerticesSceneGraphVO.create(vertices), IndicesSceneGraphVO.create(indices), ), ( ShaderShaderEntity.create(shaderName), colors |> List.map(color => Color3ContainerVO.create(color)), ), ); //用于运行测试 Js.log(Repo.getScene()); };

4、加入值对象Triangle和它的所有值对象
1)在src/domain_layer/domain/structure/math/value_object/中加入VectorMathVO.re,创建值对象Vector
VectorMathVO.re代码为:

type t = | Vector(float, float, float); let create = ((x, y, z)) => Vector(x, y, z); let value = vec => switch (vec) { | Vector(x, y, z) => (x, y, z) };

2)在src/domain_layer/domain/scene/scene_graph/value_object/中加入PositionSceneGraphVO.re,创建值对象Position
PositionSceneGraphVO.re代码为:

type t = | Position(VectorMathVO.t); let create = value => Position(value); let value = position => switch (position) { | Position(pos) => pos };

3)在src/domain_layer/domain/structure/container/value_object/中加入Color3ContainerVO.re,创建值对象Color3
Color3ContainerVO.re代码为:

type r = float; type g = float; type b = float; type t = | Color3(r, g, b); let create = ((r, g, b)) => Color3(r, g, b); let value = color => switch (color) { | Color3(r, g, b) => (r, g, b) };

4)在src/domain_layer/domain/scene/scene_graph/value_object/中加入TransformSceneGraphVO.re,创建值对象Transform
TransformSceneGraphVO.re代码为:

type t = {position: PositionSceneGraphVO.t};

5)修改GeometrySceneGraphVO.re,定义DO
GeometrySceneGraphVO.re相关代码为:

type t = { vertices: VerticesSceneGraphVO.t, indices: IndicesSceneGraphVO.t, };

6)在src/domain_layer/domain/scene/scene_graph/value_object/中加入MaterialSceneGraphVO.re,创建值对象Material
MaterialSceneGraphVO.re代码为:

type t = { shader: ShaderShaderEntity.t, colors: list(Color3ContainerVO.t), };

7)在src/domain_layer/domain/scene/scene_graph/value_object/中加入TriangleSceneGraphVO.re,创建值对象Triangle
TriangleSceneGraphVO.re代码为:

type t = { transform: TransformSceneGraphVO.t, geometry: GeometrySceneGraphVO.t, material: MaterialSceneGraphVO.t, };

5、在src/domain_layer/domain/scene/scene_graph/value_object/中加入SceneSceneGraphEntity.re,创建聚合根Scene
SceneSceneGraphEntity.re代码为:

type t = {triangles: list(TriangleSceneGraphVO.t)}; let addTriangle = (position, (vertices, indices), (shader, colors)) => { SceneRepo.addTriangle(position, (vertices, indices), (shader, colors)); };

6、在src/infrastructure_layer/data/po/中加入ScenePOType.re,定义Scene PO的类型
ScenePOType.re代码为:

type transform = {position: (float, float, float)}; type geometry = { vertices: Js.Typed_array.Float32Array.t, indices: Js.Typed_array.Uint16Array.t, }; type material = { shader: string, colors: list((float, float, float)), }; type triangle = { transform, geometry, material, }; type scene = {triangles: list(triangle)};

7、修改POType.re
POType.re相关代码为:

type po = { ... scene: ScenePOType.scene, };

8、实现Scene相关的仓库

我们按照仓库依赖关系,从上往下开始实现:
1)创建文件夹src/domain_layer/repo/scene/
2)在src/domain_layer/repo/scene/中加入SceneRepo.re,实现仓库对Scene PO的triangles字段的操作
ShaderManagerRepo.re代码为:

open ScenePOType; let _getTriangles = ({triangles}) => triangles; let addTriangle = (position, (vertices, indices), (shader, colors)) => { Repo.setScene({ ...Repo.getScene(), triangles: [ TriangleSceneRepo.create( TransformSceneRepo.create(position), GeometrySceneRepo.create(vertices, indices), MaterialSceneRepo.create(shader, colors), ), ..._getTriangles(Repo.getScene()), ], }); };

3)在src/domain_layer/repo/scene/中加入TrianglerSceneRepo.re,实现创建Scene PO的一个Triangle数据
TriangleSceneRepo.re代码为:

open ScenePOType; let create = (transform, geometry, material) => { transform, geometry, material, };

4)在src/domain_layer/repo/scene/中加入TransformSceneRepo.re,实现创建Scene PO的一个Triangle的Transform数据
TransformSceneRepo.re代码为:

open ScenePOType; let create = position => { position: position |> PositionSceneGraphVO.value |> VectorMathVO.value, };

5)在src/domain_layer/repo/scene/中加入GeometrySceneRepo.re,实现创建Scene PO的一个Triangle的Geometry数据
GeometrySceneRepo.re代码为:

open ScenePOType; let create = (vertices, indices) => { vertices: vertices |> VerticesSceneGraphVO.value, indices: indices |> IndicesSceneGraphVO.value, };

6)在src/domain_layer/repo/scene/中加入MaterialSceneRepo.re,实现创建Scene PO的一个Triangle的Material数据
MaterialSceneRepo.re代码为:

open ScenePOType; let create = (shader, colors) => { shader: shader |> ShaderShaderEntity.getId, colors: colors |> List.map(color => {color |> Color3ContainerVO.value}), };

9、修改Repo.re,实现仓库对Scene PO的操作
Repo.re相关代码为:

let getScene = () => { let po = ContainerManager.getPO(); po.scene; }; let setScene = scene => { let po = ContainerManager.getPO(); {...po, scene} |> ContainerManager.setPO; };

10、修改CreateRepo.re,实现创建Scene PO
CreateRepo.re相关代码为:

let create = () => { ... scene: { triangles: [], }, };

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

yarn webpack

12、实现index.html相关代码

index.html代码为:

 <script> ... //准备三个三角形的位置数据 var [position1, position2, position3] = [ [0.75, 0.0, 0.0], [-0.0, 0.0, 0.5], [-0.5, 0.0, -2.0] ]; //准备三个三角形的颜色数据 var [colors1, colors2, colors3] = [ [[1.0, 0.0, 0.0]], [[0.0, 0.8, 0.0], [0.0, 0.5, 0.0]], [[0.0, 0.0, 1.0]] ]; wd.Scene.addTriangle(position1, [vertices1, indices1], [shaderName1, colors1]); wd.Scene.addTriangle(position2, [vertices2, indices2], [shaderName2, colors2]); wd.Scene.addTriangle(position3, [vertices3, indices3], [shaderName1, colors3]); </script>

13、运行测试

运行index.html页面

打开控制台,可以看到打印了三次Scene PO的数据

实现“SceneJsAPI.setCamera”

1、修改SceneJsAPI.re,实现API
SceneJsAPI.re相关代码为:

let setCamera = SceneApService.setCamera;

2、修改SceneApService.re,实现应用服务
SceneApService.re相关代码为:

let setCamera = ((eye, center, up), (near, far, fovy, aspect)) => { SceneSceneGraphEntity.setCamera( ( EyeSceneGraphVO.create(eye), CenterSceneGraphVO.create(center), UpSceneGraphVO.create(up), ), ( NearSceneGraphVO.create(near), FarSceneGraphVO.create(far), FovySceneGraphVO.create(fovy), AspectSceneGraphVO.create(aspect), ), ); //用于运行测试 Js.log(Repo.getScene()); };

3、加入Camera的所有值对象
1)在src/domain_layer/domain/scene/scene_graph/value_object/中加入EyeSceneGraphVO.re、CenterSceneGraphVO.re、UpSceneGraphVO.re,创建值对象Eye、Center、Up
EyeSceneGraphVO.re代码为:

type t = | Eye(VectorMathVO.t); let create = value => Eye(value); let value = eye => switch (eye) { | Eye(value) => value };

CenterSceneGraphVO.re代码为:

type t = | Center(VectorMathVO.t); let create = value => Center(value); let value = center => switch (center) { | Center(value) => value };

UpSceneGraphVO.re代码为:

type t = | Up(VectorMathVO.t); let create = value => Up(value); let value = up => switch (up) { | Up(value) => value };

2)在src/domain_layer/domain/scene/scene_graph/value_object/中加入NearSceneGraphVO.re、FarSceneGraphVO.re、FovySceneGraphVO.re、AspectSceneGraphVO.re,创建值对象Near、Far、Fovy、Aspect
NearSceneGraphVO.re代码为:

type t = | Near(float); let create = value => Near(value); let value = near => switch (near) { | Near(value) => value };

FarSceneGraphVO.re代码为:

type t = | Far(float); let create = value => Far(value); let value = far => switch (far) { | Far(value) => value };

FovySceneGraphVO.re代码为:

type t = | Fovy(float); let create = value => Fovy(value); let value = fovy => switch (fovy) { | Fovy(value) => value };

AspectSceneGraphVO.re代码为:

type t = | Aspect(float); let create = value => Aspect(value); let value = aspect => switch (aspect) { | Aspect(value) => value };

4、在src/domain_layer/domain/scene/scene_graph/value_object/中加入CameraSceneGraphVO.re,创建值对象Camera
CameraSceneGraphVO.re代码为:

type t = { eye: EyeSceneGraphVO.t, center: CenterSceneGraphVO.t, up: UpSceneGraphVO.t, near: NearSceneGraphVO.t, far: FarSceneGraphVO.t, fovy: FovySceneGraphVO.t, aspect: AspectSceneGraphVO.t, };

5、修改SceneSceneGraphEntity.re,将Camera DO作为Scene DO 的camera字段的数据,并实现setCamera函数:
SceneSceneGraphEntity.re相关代码为:

type t = { ... camera: option(CameraSceneGraphVO.t), }; ... let setCamera = ((eye, center, up), (near, far, fovy, aspect)) => { SceneRepo.setCamera((eye, center, up), (near, far, fovy, aspect)); };

6、修改ScenePOType.re,加入Scene PO的camera字段的数据类型
ScenePOType.re相关代码为:

type camera = { eye: (float, float, float), center: (float, float, float), up: (float, float, float), near: float, far: float, fovy: float, aspect: float, }; type scene = { ... camera: option(camera), };

7、实现Scene->Camera相关的仓库

1)在src/domain_layer/repo/scene/中加入CameraSceneRepo.re,实现创建Scene PO的一个Camera数据
CameraSceneRepo.re代码为:

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

2)修改SceneRepo.re,实现仓库对Scene PO的camera字段的操作
SceneRepo.re相关代码为:

let setCamera = ((eye, center, up), (near, far, fovy, aspect)) => { Repo.setScene({ ...Repo.getScene(), camera: Some( CameraSceneRepo.create( (eye, center, up), (near, far, fovy, aspect), ), ), }); };

8、修改CreateRepo.re,实现创建Scene PO的camera字段
CreateRepo.re相关代码为:

let create = () => { ... scene: { ... camera: None, }, };

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

yarn webpack

10、实现index.html相关代码

index.html代码为:

 <script> ... //准备相机数据 var [eye, center, up] = [ [0.0, 0.0, 5.0], [0.0, 0.0, -100.0], [0.0, 1.0, 0.0] ]; var canvas = document.querySelector("#webgl"); var [near, far, fovy, aspect] = [ 1.0, 100.0, 30.0, canvas.width / canvas.height ]; wd.Scene.setCamera([eye, center, up], [near, far, fovy, aspect]); </script>

11、运行测试

运行index.html页面

打开控制台,可以看到打印了一次Scene PO的数据,它包含Camera的数据

,

诚信在线

诚信在线(www.cx11zx.cn)现已开放诚信在线手机版下载。游戏公平、公开、公正,用实力赢取信誉。

TAG:
阅读:
广告 330*360
广告 330*360
Sunbet_进入申博sunbet官网
微信二维码扫一扫
关注微信公众号
新闻自媒体 Copyright © 2002-2019 Sunbet 版权所有
二维码
意见反馈 二维码