目录
大家好,本文开始编程,实现最小的3D程序。
我们首先进行需求分析,确定功能点;
然后进行总体设计,划分模块,并且对模块进行顶层设计,给出类型签名和实现的伪代码;
最后进行具体实现,实现各个模块。
注:在Reason中,一个Reason文件(如Main.re)就是一个模块(Module)。
上一篇博文
运行测试截图
测试场景包括三个三角形:
需求分析
首先,我们分析最小3D程序的目标和特性;
接着,根据特性,我们进行头脑风暴,识别出功能关键点和扩展点;
最后,根据功能关键点和扩展点,我们确定最小3D程序的功能点。
目标
可从最小3D程序中提炼出通用的、最简化的引擎雏形
特性
为了达成目标,最小3D程序应该具备以下的特性:
- 简单
最小3D程序应该很简单,便于我们分析和提炼。 - 具有3D程序的通用特性
为了使从中提炼出的引擎雏形可扩展,最小3D程序需要包含3D程序主要的流程和通用的模式
头脑风暴
现在,我们根据特性,进行头脑风暴,识别出最小3D程序的功能关键点和扩展点。
下面从两个方面来分析:
1、从功能上分析
最简单的功能就是没有任何交互,只是绘制模型;
而最简单的模型就是三角形;
识别功能关键点:
a)绘制三角形
b)只渲染,没有任何交互
2、从流程上分析
3D程序应该包含两个步骤:
1)初始化
进一步分解,识别出最明显的子步骤:
//“|>”是函数式编程中的管道操作。例如:“A |> B”表示先执行A,然后将其返回值传给B,再执行B 初始化 = 初始化Shader |> 初始化场景
识别功能扩展点:
a)多组GLSL
因为在3D场景中,通常有各种渲染效果,如光照、雾、阴影等,每种渲染效果对应一个或多个Shader,而每个Shader对应一组GLSL,每组GLSL包含顶点GLSL和片段GLSL,所以最小3D程序需要支持多组GLSL。
2)主循环
进一步分解,识别出最明显的子步骤:
主循环 = 使用requestAnimationFrame循环执行每一帧 每一帧 = 清空画布 |> 渲染 渲染 = 设置WebGL状态 |> 设置相机 |> 绘制场景中所有的模型
识别功能扩展点:
b)多个渲染模式
3D场景往往需要用不同的模式来渲染不同的模型,如用不同的模式来渲染所有透明的模型和渲染所有非透明的模型。
c)多个WebGL状态
每个渲染模式需要设置对应的多个WebGL状态。
d)多个相机
3D场景中通常有多个相机。在渲染时,设置其中一个相机作为当前相机。
e)多个模型
3D场景往往包含多个模型。
f)每个模型有不同的Transform
Transform包括位置、旋转和缩放
确定需求
现在,我们根据功能关键点和扩展点,确定最小3D程序的需求。
下面分析非功能性需求和功能性需求:
非功能性需求
最小3D程序不考虑非功能性需求
功能性需求
我们已经识别了以下的功能关键点:
a)绘制三角形
b)只渲染,没有任何交互
结合功能关键点,我们对功能扩展点进行一一分析和决定,得到最小3D程序要实现的功能点:
a)多组GLSL
为了简单,实现两组GLSL,它们只有细微的差别,从而可以用相似的代码来渲染使用不同GLSL的三角形,减少代码复杂度
b)多个渲染模式
为了简单,只有一个渲染模式:渲染所有非透明的模型
c)多个WebGL状态
我们设置常用的两个状态:开启深度测试、开启背面剔除。
d)多个相机
为了简单,只有一个相机
e)多个模型
绘制三个三角形
f)每个模型有不同的Transform
为了简单,每个三角形有不同的位置(它们的z值,即深度不一样,从而测试“开启深度测试”的效果),不考虑旋转和缩放
根据上面的分析,我们给出最小3D程序要实现的功能点:
- 只渲染,没有交互
- 有两组GLSL
- 场景有三个三角形
第一个三角形用第一组的GLSL;
第二个三角形用第二组的GLSL;
第三个三角形用第一组的GLSL; - 所有三角形都是非透明的
- 开启深度测试和背面剔除
- 只有一个固定的透视投影相机
- 三角形的位置不同,不设置旋转和缩放
总体设计
现在,我们对最小3D程序进行总体设计:
1、我们来看下最小3D程序的上下文:
程序的逻辑放在Main模块的main函数中;
index.html页面执行main函数;
在浏览器中运行index.html页面,绘制三角形场景。
2、我们用类型签名和伪代码,对main函数进行顶层设计:
//unit表示无返回类型,类似于C语言的void type main = unit => unit; let main = () => { _init() //开启主循环 |> _loop //使用“ignore”来忽略_loop的返回值,从而使main函数的返回类型为unit |> ignore; }; //data是用于主循环的数据 type _init = unit => data; let _init = () => { 获得WebGL上下文 //因为有两组GLSL,所以有两个Shader |> 初始化所有Shader |> 初始化场景 }; type _loop = data => int; //用“rec”关键字将_loop设为递归调用 let rec _loop = (data) => requestAnimationFrame((time:int) => { //执行主循环的逻辑 _loopBody(data); //递归调用_loop _loop(data) |> ignore; }); type _loopBody = data => unit; let _loopBody = (data) => { data |> _clearCanvas |> _render }; type _render = data => unit; let _render = (data) => { 设置WebGL状态 |> 绘制三个三角形 };
具体实现
现在,我们具体实现最小3D程序,使其能够在浏览器中运行。
新建Engine3D项目
首先通过从0开发3D引擎(三):搭建开发环境,搭建Reason的开发环境;
然后新建空白的Engine3D文件夹,将Reason-Example项目的内容拷贝到该项目中,删除src/First.re文件;
在项目根目录下,依次执行“yarn install”,“yarn watch”,“yarn start”。
Engine3D项目结构为:
src/文件夹放置Reason代码;
lib/es6_global/文件夹放置编译后的js代码(使用es6 module模块规范)。
实现上下文
在src/中加入Main.re文件,定义一个空的main函数:
let main = () => { console.log("main"); };
重写index.html页面为:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>Demo</title> </head> <body> <canvas id="webgl" width="400" height="400"> Please use a browser that supports "canvas" </canvas> <script type="module"> import { main } from "./lib/es6_global/src/Main.js"; window.onload = () => { main(); }; </script> </body> </html>
index.html创建了一个canvas,并通过ES6 module引入了编译后的Main.js文件,执行main函数。
运行index.html页面
浏览器地址中输入 http://127.0.0.1:8080, 运行index.html页面。
打开浏览器控制台->Console,可以看到输出“main”。
实现_init
现在我们来实现main函数,它包括_init和_loop函数。
我们首先实现_init函数,它的总体设计为:
type _init = unit => data; let _init = () => { 获得WebGL上下文 |> 初始化所有Shader |> 初始化场景 };
实现“获得WebGL上下文”
通过以下步骤来实现:
1、获得canvas dom
需要调用window.querySelector方法来获得它 ,因此需要写FFI。
在src/中加入DomExtend.re,该文件放置与Dom交互的FFI。
在其中定义FFI:
type htmlElement = { . "width": int, "height": int, }; type body; type document = {. "body": body}; [@bs.send] external querySelector: (document, string) => htmlElement = "";
在Main.re的_init函数中,通过canvas dom id来获得canvas:
let canvas = DomExtend.querySelector(DomExtend.document, "#webgl");
2、从canvas中获得webgl1的上下文
需要调用canvas的getContext方法,因此需要写FFI。
在src/中增加Gl.re,该文件放置与webgl1 API相关的FFI。
在其中定义相关FFI:
type webgl1Context; type contextConfigJsObj = { . "alpha": bool, "depth": bool, "stencil": bool, "antialias": bool, "premultipliedAlpha": bool, "preserveDrawingBuffer": bool, }; [@bs.send] external getWebgl1Context: ('canvas, [@bs.as "webgl"] _, contextConfigJsObj) => webgl1Context = "getContext";
在Main.re的_init函数中,获得上下文,指定它的配置项:
let gl = Gl.getWebgl1Context( canvas, { "alpha": true, "depth": true, "stencil": false, "antialias": true, "premultipliedAlpha": true, "preserveDrawingBuffer": false, }: Gl.contextConfigJsObj, );
我们通过网上的资料,解释下配置项:
WebGL上下文属性:
alpha :布尔值,指示画布是否包含alpha缓冲区.
depth :布尔值,指示绘图缓冲区的深度缓冲区至少为16位.
stencil :布尔值,指示绘图缓冲区具有至少8位的模板缓冲区.
antialias :布尔值,指示是否执行抗锯齿.
premultipliedAlpha :布尔值,指示页面合成器将假定绘图缓冲区包含具有预乘alpha的颜色.
preserveDrawingBuffer :如果该值为true,则不会清除缓冲区,并且将保留其值,直到作者清除或覆盖.
failIfMajorPerformanceCaveat :布尔值,指示如果系统性能低下是否将创建上下文.
premultipliedAlpha需要设置为true,否则纹理无法进行 Texture Filtering(除非使用最近邻插值)。具体可以参考Premultiplied Alpha 到底是干嘛用的
这里忽略了“failIfMajorPerformanceCaveat“。
实现“初始化所有Shader”
一共有两个Shader,分别对应一组GLSL。
- 在src/中加入GLSL.re,定义两组GLSL
GLSL.re:
let 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); } |}; let fs1 = {| precision mediump float; uniform vec3 u_color0; void main(){ gl_FragColor = vec4(u_color0,1.0); } |}; let 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); } |}; let fs2 = {| precision mediump float; uniform vec3 u_color0; uniform vec3 u_color1; void main(){ gl_FragColor = vec4(u_color0 * u_color1,1.0); } |};
这两组GLSL类似,它们的顶点GLSL一样,都传入了model、view、projection矩阵和三角形的顶点坐标a_position;
它们的片段GLSL有细微的差别:第一个的片段GLSL只传入了一个颜色u_color0,第二个的片段GLSL传入了两个颜色u_color0、u_color1。
- 在Gl.re中定义FFI
Gl.re:
type program; type shader; [@bs.send.pipe: webgl1Context] external createProgram: program = ""; [@bs.send.pipe: webgl1Context] external linkProgram: program => unit = ""; [@bs.send.pipe: webgl1Context] external shaderSource: (shader, string) => unit = ""; [@bs.send.pipe: webgl1Context] external compileShader: shader => unit = ""; [@bs.send.pipe: webgl1Context] external createShader: int => shader = ""; [@bs.get] external getVertexShader: webgl1Context => int = "VERTEX_SHADER"; [@bs.get] external getFragmentShader: webgl1Context => int = "FRAGMENT_SHADER"; [@bs.get] external getCompileStatus: webgl1Context => int = "COMPILE_STATUS"; [@bs.get] external getLinkStatus: webgl1Context => int = "LINK_STATUS"; [@bs.send.pipe: webgl1Context] external getProgramParameter: (program, int) => bool = ""; [@bs.send.pipe: webgl1Context] external getShaderInfoLog: shader => string = ""; [@bs.send.pipe: webgl1Context] external getProgramInfoLog: program => string = ""; [@bs.send.pipe: webgl1Context] external attachShader: (program, shader) => unit = ""; [@bs.send.pipe: webgl1Context] external bindAttribLocation: (program, int, string) => unit = ""; [@bs.send.pipe: webgl1Context] external deleteShader: shader => unit = "";
- 传入对应的GLSL,初始化两个shader,创建并获得两个program
因为"初始化Shader"是通用逻辑,因此在Main.re的_init函数中提出该函数。
Main.re的_init函数的相关代码如下:
//通过抛出异常来处理错误 let error = msg => Js.Exn.raiseError(msg) |> ignore; let _compileShader = (gl, glslSource: string, shader) => { Gl.shaderSource(shader, glslSource, gl); Gl.compileShader(shader, gl); Gl.getShaderParameter(shader, Gl.getCompileStatus(gl), gl) === false ? { let message = Gl.getShaderInfoLog(shader, gl); error( {j|shader info log: $message glsl source: $glslSource |j}, ); } : (); shader; }; let _linkProgram = (program, gl) => { Gl.linkProgram(program, gl); Gl.getProgramParameter(program, Gl.getLinkStatus(gl), gl) === false ? { let message = Gl.getProgramInfoLog(program, gl); error({j|link program error: $message|j}); } : (); }; let initShader = (vsSource: string, fsSource: string, gl, program) => { let vs = _compileShader( gl, vsSource, Gl.createShader(Gl.getVertexShader(gl), gl), ); let fs = _compileShader( gl, fsSource, Gl.createShader(Gl.getFragmentShader(gl), gl), ); Gl.attachShader(program, vs, gl); Gl.attachShader(program, fs, gl); //需要确保attribute 0 enabled,具体原因可参考: http://stackoverflow.com/questions/20305231/webgl-warning-attribute-0-is-disabled-this-has-significant-performance-penalt?answertab=votes#tab-top Gl.bindAttribLocation(program, 0, "a_position", gl); _linkProgram(program, gl); Gl.deleteShader(vs, gl); Gl.deleteShader(fs, gl); program; }; let program1 = gl |> Gl.createProgram |> initShader(GLSL.vs1, GLSL.fs1, gl); let program2 = gl |> Gl.createProgram |> initShader(GLSL.vs2, GLSL.fs2, gl);
因为error和initShader函数属于辅助逻辑,所以我们进行重构,在src/中加入Utils.re,将其移到其中。
实现“初始化场景”
我们在后面实现“渲染”时,要使用drawElements来绘制三角形,因此在这里不仅需要创建三角形的vertices数据,还需要创建三角形的indices数据。
另外,我们决定使用VBO来保存三角形的顶点数据。
值得说明的是,我们使用“Geometry”这个概念来指代模型的Mesh结构,Geometry数据就是指三角形的顶点数据,包括vertices、indices等数据。
我们来细化“初始化场景”:
初始化场景 = 创建三个三角形的Geometry数据 |> 创建和初始化对应的VBO |> 设置相机的view matrix和projection matrix |> 设置清空颜色缓冲时的颜色值
下面分别实现子逻辑:
- 创建三个三角形的Geometry数据
因为每个三角形的Geometry数据都一样,所以在Utils.re中增加通用的createTriangleGeometryData函数:
let createTriangleGeometryData = () => { 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, |]); let indices = Uint16Array.make([|0, 1, 2|]); (vertices, indices); };
这里使用Reason提供的Js.Typed_array.Float32Array库来操作Float32Array。
在Main.re的_init函数中,创建三个三角形的Geometry数据:
let (vertices1, indices1) = Utils.createTriangleGeometryData(); let (vertices2, indices2) = Utils.createTriangleGeometryData(); let (vertices3, indices3) = Utils.createTriangleGeometryData();
- 创建和初始化对应的VBO
在Gl.re中定义FFI:
type bufferTarget = | ArrayBuffer | ElementArrayBuffer; type usage = | Static; [@bs.send.pipe: webgl1Context] external createBuffer: buffer = ""; [@bs.get] external getArrayBuffer: webgl1Context => bufferTarget = "ARRAY_BUFFER"; [@bs.get] external getElementArrayBuffer: webgl1Context => bufferTarget = "ELEMENT_ARRAY_BUFFER"; [@bs.send.pipe: webgl1Context] external bindBuffer: (bufferTarget, buffer) => unit = ""; [@bs.send.pipe: webgl1Context] external bufferFloat32Data: (bufferTarget, Float32Array.t, usage) => unit = "bufferData"; [@bs.send.pipe: webgl1Context] external bufferUint16Data: (bufferTarget, Uint16Array.t, usage) => unit = "bufferData"; [@bs.get] external getStaticDraw: webgl1Context => usage = "STATIC_DRAW";
因为每个三角形“创建和初始化VBO”的逻辑都一样,所以在Utils.re中增加通用的initVertexBuffers函数:
let initVertexBuffers = ((vertices, indices), gl) => { let vertexBuffer = Gl.createBuffer(gl); Gl.bindBuffer(Gl.getArrayBuffer(gl), vertexBuffer, gl); Gl.bufferFloat32Data( Gl.getArrayBuffer(gl), vertices, Gl.getStaticDraw(gl), gl, ); let indexBuffer = Gl.createBuffer(gl); Gl.bindBuffer(Gl.getElementArrayBuffer(gl), indexBuffer, gl); Gl.bufferUint16Data( Gl.getElementArrayBuffer(gl), indices, Gl.getStaticDraw(gl), gl, ); (vertexBuffer, indexBuffer); };
在Main.re的_init函数中,创建和初始化对应的VBO:
let (vertexBuffer1, indexBuffer1) = Utils.initVertexBuffers((vertices1, indices1), gl); let (vertexBuffer2, indexBuffer2) = Utils.initVertexBuffers((vertices2, indices2), gl); let (vertexBuffer3, indexBuffer3) = Utils.initVertexBuffers((vertices3, indices3), gl);
- 设置相机的view matrix和projection matrix
因为涉及到矩阵操作,并且该矩阵操作需要操作Vector,所以我们在src/中加入Matrix.re和Vector.re,增加对应的函数:
Matrix.re:
open Js.Typed_array; let createIdentityMatrix = () => Js.Typed_array.Float32Array.make([| 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., |]); let _getEpsilon = () => 0.000001; let setLookAt = ( (eyeX, eyeY, eyeZ) as eye, (centerX, centerY, centerZ) as center, (upX, upY, upZ) as up, resultFloat32Arr, ) => Js.Math.abs_float(eyeX -. centerX) < _getEpsilon() && Js.Math.abs_float(eyeY -. centerY) < _getEpsilon() && Js.Math.abs_float(eyeZ -. centerZ) < _getEpsilon() ? resultFloat32Arr : { let (z1, z2, z3) as z = Vector.sub(eye, center) |> Vector.normalize; let (x1, x2, x3) as x = Vector.cross(up, z) |> Vector.normalize; let (y1, y2, y3) as y = Vector.cross(z, x) |> Vector.normalize; 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, -. Vector.dot(x, eye)); Float32Array.unsafe_set(resultFloat32Arr, 13, -. Vector.dot(y, eye)); Float32Array.unsafe_set(resultFloat32Arr, 14, -. Vector.dot(z, eye)); Float32Array.unsafe_set(resultFloat32Arr, 15, 1.); resultFloat32Arr; }; let buildPerspective = ((fovy: float, aspect: float, near: float, far: float), resultFloat32Arr) => { Js.Math.sin(Js.Math._PI *. fovy /. 180. /. 2.) === 0. ? Utils.error("frustum should not be null") : (); 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; 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; };
Vector.re:
let dot = ((x, y, z), (vx, vy, vz)) => x *. vx +. y *. vy +. z *. vz; let sub = ((x1, y1, z1), (x2, y2, z2)) => (x1 -. x2, y1 -. y2, z1 -. z2); let scale = (scalar, (x, y, z)) => (x *. scalar, y *. scalar, z *. scalar); let cross = ((x1, y1, z1), (x2, y2, z2)) => ( y1 *. z2 -. y2 *. z1, z1 *. x2 -. z2 *. x1, x1 *. y2 -. x2 *. y1, ); let normalize = ((x, y, z)) => { let d = Js.Math.sqrt(x *. x +. y *. y +. z *. z); d === 0. ? (0., 0., 0.) : (x /. d, y /. d, z /. d); };
在Main.re的_init函数中,设置固定相机的vMatrix和pMatrix:
let vMatrix = Matrix.createIdentityMatrix() |> Matrix.setLookAt((0., 0.0, 5.), (0., 0., (-100.)), (0., 1., 0.)); let pMatrix = Matrix.createIdentityMatrix() |> Matrix.buildPerspective(( 30., (canvas##width |> Js.Int.toFloat) /. (canvas##height |> Js.Int.toFloat), 1., 100., ));
实现“设置清空颜色缓冲时的颜色值”
在Gl.re中定义FFI:
[@bs.send.pipe: webgl1Context] external clearColor: (float, float, float, float) => unit = "";
在Main.re的_init函数中,设置清空颜色缓冲时的颜色值为黑色:
Gl.clearColor(0., 0., 0., 1., gl);
返回用于主循环的数据
在Main.re的_init函数中,将WebGL上下文、所有的program、所有的indices、所有的VBO、相机的view matrix和projection matrix返回,供主循环使用(只可读):
( gl, (program1, program2), (indices1, indices2, indices3), (vertexBuffer1, indexBuffer1), (vertexBuffer2, indexBuffer2), (vertexBuffer3, indexBuffer3), (vMatrix, pMatrix), );
实现_loop
_init函数实现完毕,接下来实现_loop函数,它的总体设计为:
type _loop = data => int; let rec _loop = (data) => requestAnimationFrame((time:int) => { _loopBody(data); _loop(data) |> ignore; });
实现“主循环”
需要调用window.requestAnimationFrame来开启主循环。
在DomExtend.re中定义FFI:
[@bs.val] external requestAnimationFrame: (float => unit) => int = "";
然后定义空函数_loopBody,实现_loop的主循环,并通过编译检查:
let _loopBody = (data) => (); let rec _loop = data => DomExtend.requestAnimationFrame((time: float) => { _loopBody(data); _loop(data) |> ignore; });
实现“clearCanvas”
接下来我们要实现_loopBody,它的总体设计为:
type _loopBody = data => unit; let _loopBody = (data) => { data |> _clearCanvas |> _render };
我们首先实现_clearCanvas函数,为此需要在Gl.re中定义FFI:
[@bs.send.pipe: webgl1Context] external clear: int => unit = ""; [@bs.get] external getColorBufferBit: webgl1Context => int = "COLOR_BUFFER_BIT"; [@bs.get] external getDepthBufferBit: webgl1Context => int = "DEPTH_BUFFER_BIT";
然后在Main.re中实现_clearCanvas函数:
let _clearCanvas = ( ( gl, (program1, program2), (indices1, indices2, indices3), (vertexBuffer1, indexBuffer1), (vertexBuffer2, indexBuffer2), (vertexBuffer3, indexBuffer3), (vMatrix, pMatrix), ) as data, ) => { Gl.clear(Gl.getColorBufferBit(gl) lor Gl.getDepthBufferBit(gl), gl); data; };
实现“_render”
_render的总体设计为:
type _render = data => unit; let _render = (data) => { 设置WebGL状态 |> 绘制三个三角形 };
下面分别实现:
设置WebGL状态
在Gl.re中定义FFI:
[@bs.get] external getDepthTest: webgl1Context => int = "DEPTH_TEST"; [@bs.send.pipe: webgl1Context] external enable: int => unit = ""; [@bs.get] external getCullFace: webgl1Context => int = "CULL_FACE"; [@bs.send.pipe: webgl1Context] external cullFace: int => unit = ""; [@bs.get] external getBack: webgl1Context => int = "BACK";
在Main.re的_render函数中设置WebGL状态,开启深度测试和背面剔除:
Gl.enable(Gl.getDepthTest(gl), gl); Gl.enable(Gl.getCullFace(gl), gl); Gl.cullFace(Gl.getBack(gl), gl);
绘制第一个三角形
在_render函数中需要绘制三个三角形。
我们来细化“绘制每个三角形”:
绘制每个三角形 = 使用对应的Program |> 传递三角形的顶点数据 |> 传递相机数据 |> 传递三角形的位置数据 |> 传递三角形的颜色数据 |> 绘制三角形
下面先绘制第一个三角形,分别实现它的子逻辑:
- 使用对应的Program
在Gl.re中定义FFI:
[@bs.send.pipe: webgl1Context] external useProgram: program => unit = "";
在Main.re的_render函数中使用program1:
Gl.useProgram(program1, gl);
- 传递三角形的顶点数据
在Gl.re中定义FFI:
type attributeLocation = int; [@bs.send.pipe: webgl1Context] external getAttribLocation: (program, string) => attributeLocation = ""; [@bs.send.pipe: webgl1Context] external vertexAttribPointer: (attributeLocation, int, int, bool, int, int) => unit = ""; [@bs.send.pipe: webgl1Context] external enableVertexAttribArray: attributeLocation => unit = ""; [@bs.get] external getFloat: webgl1Context => int = "FLOAT";
因为“传递顶点数据”是通用逻辑,所以在Utils.re中增加sendAttributeData函数:
首先判断program对应的GLSL中是否有vertices对应的attribute:a_position;
如果有,则开启vertices对应的VBO;否则,抛出错误信息。
相关代码如下:
let sendAttributeData = (vertexBuffer, program, gl) => { let positionLocation = Gl.getAttribLocation(program, "a_position", gl); positionLocation === (-1) ? error({j|Failed to get the storage location of a_position|j}) : (); Gl.bindBuffer(Gl.getArrayBuffer(gl), vertexBuffer, gl); Gl.vertexAttribPointer( positionLocation, 3, Gl.getFloat(gl), false, 0, 0, gl, ); Gl.enableVertexAttribArray(positionLocation, gl); };
在Main.re的_render函数中调用sendAttributeData:
Utils.sendAttributeData(vertexBuffer1, program1, gl);
- 传递相机数据
在Gl.re中定义FFI:
type uniformLocation; [@bs.send.pipe: webgl1Context] external uniformMatrix4fv: (uniformLocation, bool, Float32Array.t) => unit = ""; [@bs.send.pipe: webgl1Context] external getUniformLocation: (program, string) => Js.Null.t(uniformLocation) = "";
因为“传递相机数据”是通用逻辑,所以在Utils.re中增加sendCameraUniformData函数:
首先判断program对应的GLSL中是否有view matrix对应的uniform:u_vMatrix和projection matrix对应的uniform:u_pMatrix;
如果有,则传递对应的矩阵数据;否则,抛出错误信息。
相关代码如下:
//与error函数的不同是没有使用ignore来忽略返回值 let errorAndReturn = msg => Js.Exn.raiseError(msg); let _unsafeGetUniformLocation = (program, name, gl) => switch (Gl.getUniformLocation(program, name, gl)) { | pos when !Js.Null.test(pos) => Js.Null.getUnsafe(pos) //这里需要有返回值 | _ => errorAndReturn({j|$name uniform not exist|j}) }; let sendCameraUniformData = ((vMatrix, pMatrix), program, gl) => { let vMatrixLocation = _unsafeGetUniformLocation(program, "u_vMatrix", gl); let pMatrixLocation = _unsafeGetUniformLocation(program, "u_pMatrix", gl); Gl.uniformMatrix4fv(vMatrixLocation, false, vMatrix, gl); Gl.uniformMatrix4fv(pMatrixLocation, false, pMatrix, gl); };
在Main.re的_render函数中调用sendCameraUniformData:
Utils.sendCameraUniformData((vMatrix, pMatrix), program1, gl);
- “传递三角形的位置数据”以及“传递三角形的颜色数据”
在Gl.re中定义FFI:
[@bs.send.pipe: webgl1Context] external uniform3f: (uniformLocation, float, float, float) => unit = "";
因为这两个逻辑都是传递GLSL的uniform数据,所以放在一个函数中;又因为使用不同GLSL的三角形,传递的颜色数据不一样,所以需要在Utils.re中,增加sendModelUniformData1、sendModelUniformData2函数,分别对应第一组和第二组GLSL。第一个和第三个三角形使用sendModelUniformData1,第二个三角形使用sendModelUniformData2。
这两个函数都需要判断GLSL中是否有model matrix对应的uniform:u_mMatrix和颜色对应的uniform;
如果有,则传递对应的数据;否则,抛出错误信息。
相关代码如下:
let _sendColorData = ((r, g, b), gl, colorLocation) => Gl.uniform3f(colorLocation, r, g, b, gl); let sendModelUniformData1 = ((mMatrix, color), program, gl) => { let mMatrixLocation = _unsafeGetUniformLocation(program, "u_mMatrix", gl); let colorLocation = _unsafeGetUniformLocation(program, "u_color0", gl); Gl.uniformMatrix4fv(mMatrixLocation, false, mMatrix, gl); _sendColorData(color, gl, colorLocation); }; let sendModelUniformData2 = ((mMatrix, color1, color2), program, gl) => { let mMatrixLocation = _unsafeGetUniformLocation(program, "u_mMatrix", gl); let color1Location = _unsafeGetUniformLocation(program, "u_color0", gl); let color2Location = _unsafeGetUniformLocation(program, "u_color1", gl); Gl.uniformMatrix4fv(mMatrixLocation, false, mMatrix, gl); _sendColorData(color1, gl, color1Location); _sendColorData(color2, gl, color2Location); };
在Matrix.re中增加setTranslation函数:
let setTranslation = ((x, y, z), resultFloat32Arr) => { Float32Array.unsafe_set(resultFloat32Arr, 12, x); Float32Array.unsafe_set(resultFloat32Arr, 13, y); Float32Array.unsafe_set(resultFloat32Arr, 14, z); resultFloat32Arr; };
在Main.re的_render函数中调用sendModelUniformData1:
Utils.sendModelUniformData1( ( Matrix.createIdentityMatrix() |> Matrix.setTranslation((0.75, 0., 0.)), (1., 0., 0.), ), program1, gl, );
- 绘制三角形
在Gl.re中定义FFI:
[@bs.get] external getTriangles: webgl1Context => int = "TRIANGLES"; [@bs.get] external getUnsignedShort: webgl1Context => int = "UNSIGNED_SHORT"; [@bs.send.pipe: webgl1Context] external drawElements: (int, int, int, int) => unit = "";
在Main.re的_render函数中,绑定indices1对应的VBO,使用drawElements绘制第一个三角形:
Gl.bindBuffer(Gl.getElementArrayBuffer(gl), indexBuffer1, gl); Gl.drawElements( Gl.getTriangles(gl), indices1 |> Js.Typed_array.Uint16Array.length, Gl.getUnsignedShort(gl), 0, gl, );
绘制第二个和第三个三角形
与绘制第一个三角形类似,在Main.re的_render函数中,使用对应的program,传递相同的相机数据,调用对应的Utils.sendModelUniformData1或sendModelUniformData2函数、绑定对应的VBO,来绘制第二个和第三个三角形。
Main.re的_render函数的相关代码如下:
//绘制第二个三角形 Gl.useProgram(program2, gl); Utils.sendAttributeData(vertexBuffer2, program2, gl); Utils.sendCameraUniformData((vMatrix, pMatrix), program2, gl); Utils.sendModelUniformData2( ( Matrix.createIdentityMatrix() |> Matrix.setTranslation(((-0.), 0., 0.5)), (0., 0.8, 0.), (0., 0.5, 0.), ), program2, gl, ); Gl.bindBuffer(Gl.getElementArrayBuffer(gl), indexBuffer2, gl); Gl.drawElements( Gl.getTriangles(gl), indices2 |> Js.Typed_array.Uint16Array.length, Gl.getUnsignedShort(gl), 0, gl, ); //绘制第三个三角形 Gl.useProgram(program1, gl); Utils.sendAttributeData(vertexBuffer3, program1, gl); Utils.sendCameraUniformData((vMatrix, pMatrix), program1, gl); Utils.sendModelUniformData1( ( Matrix.createIdentityMatrix() |> Matrix.setTranslation(((-0.5), 0., (-2.))), (0., 0., 1.), ), program1, gl, ); Gl.bindBuffer(Gl.getElementArrayBuffer(gl), indexBuffer3, gl); Gl.drawElements( Gl.getTriangles(gl), indices3 |> Js.Typed_array.Uint16Array.length, Gl.getUnsignedShort(gl), 0, gl, );
最终的分层和领域模型
如下图所示:
总结
本文通过需求分析、总体设计和具体实现,实现了最小的3D程序,绘制了三角形。
但是,还有很多不足之处:
1、场景逻辑和WebGL API的调用逻辑混杂在一起
2、存在重复代码,如Utils的sendModelUniformData1和sendModelUniformData2有重复的模式
3、需要进行优化,如只需要传递一次相机数据、“使用getShaderParameter来检查初始化Shader的正确性”降低了性能
4、_init传递给主循环的数据,作为函数的形参过于复杂
我们会在后面的文章中,解决这些问题。
本文完整代码地址
Book-Demo-Triangle Github Repo
来源:https://www.cnblogs.com/chaogex/p/12234673.html