目录html
你们好,本文开始编程,实现最小的3D程序。git
咱们首先进行需求分析,肯定功能点;
而后进行整体设计,划分模块,而且对模块进行顶层设计,给出类型签名和实现的伪代码;
最后进行具体实现,实现各个模块。es6
注:在Reason中,一个Reason文件(如Main.re)就是一个模块(Module)。github
测试场景包括三个三角形:
编程
首先,咱们分析最小3D程序的目标和特性;
接着,根据特性,咱们进行头脑风暴,识别出功能关键点和扩展点;
最后,根据功能关键点和扩展点,咱们肯定最小3D程序的功能点。canvas
可从最小3D程序中提炼出通用的、最简化的引擎雏形浏览器
为了达成目标,最小3D程序应该具有如下的特性:dom
如今,咱们根据特性,进行头脑风暴,识别出最小3D程序的功能关键点和扩展点。函数式编程
下面从两个方面来分析:
一、从功能上分析
最简单的功能就是没有任何交互,只是绘制模型;
而最简单的模型就是三角形;
识别功能关键点:
a)绘制三角形
b)只渲染,没有任何交互
二、从流程上分析
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程序要实现的功能点:
如今,咱们对最小3D程序进行整体设计:
一、咱们来看下最小3D程序的上下文:
程序的逻辑放在Main模块的main函数中;
index.html页面执行main函数;
在浏览器中运行index.html页面,绘制三角形场景。
二、咱们用类型签名和伪代码,对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程序,使其可以在浏览器中运行。
首先经过从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”。
如今咱们来实现main函数,它包括_init和_loop函数。
咱们首先实现_init函数,它的整体设计为:
type _init = unit => data; let _init = () => { 得到WebGL上下文 |> 初始化全部Shader |> 初始化场景 };
经过如下步骤来实现:
一、得到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");
二、从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,分别对应一组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:
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 = "";
由于"初始化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数据都同样,因此在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();
在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);
由于涉及到矩阵操做,而且该矩阵操做须要操做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), );
_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; });
接下来咱们要实现_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的整体设计为:
type _render = data => unit; let _render = (data) => { 设置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 |> 传递三角形的顶点数据 |> 传递相机数据 |> 传递三角形的位置数据 |> 传递三角形的颜色数据 |> 绘制三角形
下面先绘制第一个三角形,分别实现它的子逻辑:
在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中,增长sendModelUniformData一、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程序,绘制了三角形。
可是,还有不少不足之处:
一、场景逻辑和WebGL API的调用逻辑混杂在一块儿
二、存在重复代码,如Utils的sendModelUniformData1和sendModelUniformData2有重复的模式
三、须要进行优化,如只须要传递一次相机数据、“使用getShaderParameter来检查初始化Shader的正确性”下降了性能
四、_init传递给主循环的数据,做为函数的形参过于复杂
咱们会在后面的文章中,解决这些问题。