从0开发3D引擎(九):实现最小的3D程序-“绘制三角形”

你们好,本文开始编程,实现最小的3D程序。git

咱们首先进行需求分析,肯定功能点;
而后进行整体设计,划分模块,而且对模块进行顶层设计,给出类型签名和实现的伪代码;
最后进行具体实现,实现各个模块。es6

注:在Reason中,一个Reason文件(如Main.re)就是一个模块(Module)。github

上一篇博文

从0开发3D引擎(八):准备“搭建引擎雏形”web

运行测试截图

测试场景包括三个三角形:
下载.png-6.5kB编程

需求分析

首先,咱们分析最小3D程序的目标和特性;
接着,根据特性,咱们进行头脑风暴,识别出功能关键点和扩展点;
最后,根据功能关键点和扩展点,咱们肯定最小3D程序的功能点。canvas

目标

可从最小3D程序中提炼出通用的、最简化的引擎雏形浏览器

特性

为了达成目标,最小3D程序应该具有如下的特性:dom

  • 简单
    最小3D程序应该很简单,便于咱们分析和提炼。
  • 具备3D程序的通用特性
    为了使从中提炼出的引擎雏形可扩展,最小3D程序须要包含3D程序主要的流程和通用的模式

头脑风暴

如今,咱们根据特性,进行头脑风暴,识别出最小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程序要实现的功能点:

  • 只渲染,没有交互
  • 有两组GLSL
  • 场景有三个三角形
    第一个三角形用第一组的GLSL;
    第二个三角形用第二组的GLSL;
    第三个三角形用第一组的GLSL;
  • 全部三角形都是非透明的
  • 开启深度测试和背面剔除
  • 只有一个固定的透视投影相机
  • 三角形的位置不一样,不设置旋转和缩放

整体设计

如今,咱们对最小3D程序进行整体设计:

一、咱们来看下最小3D程序的上下文:
实现最小的3D程序-“绘制三角形”:页面调用.png-8.3kB

程序的逻辑放在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程序,使其可以在浏览器中运行。

新建Engine3D项目

首先经过从0开发3D引擎(三):搭建开发环境,搭建Reason的开发环境;
而后新建空白的Engine3D文件夹,将Reason-Example项目的内容拷贝到该项目中,删除src/First.re文件;
在项目根目录下,依次执行“yarn install”,“yarn watch”,“yarn start”。

Engine3D项目结构为:
截屏2020-01-26上午10.41.55.png-58kB

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上下文”

经过如下步骤来实现:
一、得到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”

一共有两个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中,增长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程序-“绘制三角形”:最终领域模型 (1).png-75kB

总结

本文经过需求分析、整体设计和具体实现,实现了最小的3D程序,绘制了三角形。

可是,还有不少不足之处:
一、场景逻辑和WebGL API的调用逻辑混杂在一块儿
二、存在重复代码,如Utils的sendModelUniformData1和sendModelUniformData2有重复的模式
三、须要进行优化,如只须要传递一次相机数据、“使用getShaderParameter来检查初始化Shader的正确性”下降了性能
四、_init传递给主循环的数据,做为函数的形参过于复杂

咱们会在后面的文章中,解决这些问题。

本文完整代码地址

Book-Demo-Triangle Github Repo

相关文章
相关标签/搜索