目錄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傳遞給主循環的數據,做爲函數的形參過於複雜
咱們會在後面的文章中,解決這些問題。