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


    大家好,本文开始编程,实现最小的3D程序。

    我们首先进行需求分析,确定功能点;
    然后进行事件风暴,建立领域驱动设计的通用语言;
    然后进行初步实现,划分顶层模块,进行顶层实现,给出类型签名和实现的伪代码;
    然后进行具体实现,从顶层到底层依次实现;
    最后更新通用语言。

    上一篇博文

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

    下一篇博文

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

    最终效果截图

    此处输入图片的描述

    术语解释

    • 文件与模块
      在Reason中,一个Reason文件(如Main.re)就是一个模块(Module),所以我们既称Main.re为Main文件,也称它为Main模块。

    需求分析

    首先,我们分析最小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)每个模型有不同的位置、旋转和缩放

    确定需求

    现在,我们根据功能关键点和扩展点,确定最小3D程序的需求。

    下面分析非功能性需求和功能性需求:

    非功能性需求
    最小3D程序不考虑非功能性需求

    功能性需求
    我们已经识别了以下的功能关键点:
    a)渲染三角形
    b)只渲染,没有任何交互

    结合功能关键点,我们对功能扩展点进行一一分析和决定,得到最小3D程序要实现的功能点:
    a)多组GLSL
    为了简单,实现两组GLSL,它们只有细微的差别,从而可以用相似的代码来渲染使用不同GLSL的三角形,减少代码复杂度
    b)多个渲染模式
    为了简单,只有一个渲染模式:渲染所有非透明的模型
    c)多个WebGL状态
    我们设置常用的两个状态:开启深度测试、开启背面剔除。
    d)多个相机
    为了简单,只有一个相机
    e)多个模型
    渲染三个三角形
    f)每个模型有不同的位置、旋转和缩放
    为了简单,每个三角形有不同的位置(它们的z值,即深度不一样,从而测试“开启深度测试”的效果),但不考虑旋转和缩放

    根据上面的分析,我们给出最小3D程序要实现的功能点:

    • 只渲染,没有交互
    • 有两组GLSL
      它们的区别为:
      第一组GLSL接收一个颜色的uniform;
      第二组GLSL接收两个颜色的uniform。
    • 场景有三个三角形,对应不同的GLSL
      第一个三角形有一个颜色数据,用第一组的GLSL;
      第二个三角形有两个颜色数据,用第二组的GLSL;
      第三个三角形有一个颜色数据,用第一组的GLSL;
    • 所有三角形都是非透明的
    • 开启深度测试和背面剔除
    • 只有一个固定的透视投影相机
    • 三角形的位置不同,不考虑旋转和缩放

    事件风暴

    我们在分析的需求的基础上,进行事件风暴,建立通用语言。

    识别领域事件

    实现最小的3D程序-“绘制三角形”:事件风暴-识别领域事件 (1).png-40.1kB

    识别命令,寻找聚合,划分限界上下文

    此处输入图片的描述

    通用语言

    此处输入图片的描述

    初步实现

    现在,我们对最小3D程序进行初步实现:

    1、划分最小3D程序的顶层模块:
    实现最小的3D程序-“绘制三角形”:页面调用.png-8.3kB

    程序的逻辑放在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
        |> _clearColor
        |> _clearCanvas
        |> _render
    };
    
    type _render = data => unit;
    let _render = (data) => {
        设置WebGL状态 
        |> 渲染三个三角形
    };
    

    具体实现

    现在,我们从顶层到底层依次实现最小3D程序,使其能够在浏览器中运行。

    新建TinyWonder项目

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

    TinyWonder项目结构为:
    截屏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上下文”

    通过以下步骤来实现:
    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 =
    WebGL1.getWebGL1Context(
      canvas,
      {
        "alpha": true,
        "depth": true,
        "stencil": false,
        "antialias": true,
        "premultipliedAlpha": true,
        "preserveDrawingBuffer": false,
      }: WebGL1.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

    WebGL1.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);
    
    let _compileShader = (gl, glslSource: string, shader) => {
      WebGL1.shaderSource(shader, glslSource, gl);
      WebGL1.compileShader(shader, gl);
    
      WebGL1.getShaderParameter(shader, WebGL1.getCompileStatus(gl), gl) === false
        ? {
          let message = WebGL1.getShaderInfoLog(shader, gl);
    
          error(
            {j|shader info log: $message
            glsl source: $glslSource
            |j},
          );
        }
        : ();
    
      shader;
    };
    
    let _linkProgram = (program, gl) => {
      WebGL1.linkProgram(program, gl);
    
      WebGL1.getProgramParameter(program, WebGL1.getLinkStatus(gl), gl) === false
        ? {
          let message = WebGL1.getProgramInfoLog(program, gl);
    
          error({j|link program error: $message|j});
        }
        : ();
    };
    
    let initShader = (vsSource: string, fsSource: string, gl, program) => {
      let vs =
        _compileShader(
          gl,
          vsSource,
          WebGL1.createShader(WebGL1.getVertexShader(gl), gl),
        );
      let fs =
        _compileShader(
          gl,
          fsSource,
          WebGL1.createShader(WebGL1.getFragmentShader(gl), gl),
        );
    
      WebGL1.attachShader(program, vs, gl);
      WebGL1.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
      WebGL1.bindAttribLocation(program, 0, "a_position", gl);
    
      _linkProgram(program, gl);
    
      WebGL1.deleteShader(vs, gl);
      WebGL1.deleteShader(fs, gl);
    
      program;
    };
    
    let program1 =
    gl |> WebGL1.createProgram |> initShader(GLSL.vs1, GLSL.fs1, gl);
    
    let program2 =
    gl |> WebGL1.createProgram |> initShader(GLSL.vs2, GLSL.fs2, gl);
    

    因为error和initShader函数属于辅助逻辑,所以我们进行重构,在src/中加入Utils.re,将其移到其中。

    实现“初始化场景”

    我们需要创建场景数据,“渲染”时使用。

    我们在后面实现“渲染”时,要使用drawElements来绘制三角形,因此在这里不仅需要创建三角形的vertices数据,还需要创建三角形的indices数据。

    我们把vertices、indices数据统称为“顶点数据”。

    另外,我们决定使用VBO来保存三角形的顶点数据,因此在这里要创建和初始化每个三角形的VBO。

    我们来细化“初始化场景”:

    初始化场景 = 创建三个三角形的顶点数据 |>  创建和初始化对应的VBO |> 创建三个三角形的位置数据 |> 创建三个三角形的颜色数据 |> 创建相机view matrix需要的数据和projection matrix需要的数据
    

    下面分别实现子逻辑:

    • 创建三个三角形的顶点数据

    因为每个三角形的顶点数据都一样,所以在Utils.re中增加通用的createTriangleVertexData函数:

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

    这里使用Reason提供的Js.Typed_array.Float32Array库来操作Float32Array。

    在Main.re的_init函数中,创建三个三角形的顶点数据:

    let (vertices1, indices1) = Utils.createTriangleVertexData();
    let (vertices2, indices2) = Utils.createTriangleVertexData();
    let (vertices3, indices3) = Utils.createTriangleVertexData();
    
    • 创建和初始化对应的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 = WebGL1.createBuffer(gl);
    
      WebGL1.bindBuffer(WebGL1.getArrayBuffer(gl), vertexBuffer, gl);
    
      WebGL1.bufferFloat32Data(
        WebGL1.getArrayBuffer(gl),
        vertices,
        WebGL1.getStaticDraw(gl),
        gl,
      );
    
      let indexBuffer = WebGL1.createBuffer(gl);
    
      WebGL1.bindBuffer(WebGL1.getElementArrayBuffer(gl), indexBuffer, gl);
    
      WebGL1.bufferUint16Data(
        WebGL1.getElementArrayBuffer(gl),
        indices,
        WebGL1.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);
    
    • 创建三个三角形的位置数据

    三角形的位置数据为本地坐标系中的x、y、z坐标。

    在Main.re的_init函数中,创建三个三角形的位置数据:

    let (position1, position2, position3) = (
        (0.75, 0., 0.),
        ((-0.), 0., 0.5),
        ((-0.5), 0., (-2.)),
    );
    
    • 创建三个三角形的颜色数据

    在Main.re的_init函数中,创建三个三角形的颜色数据:

       //第一个和第三个三角形只有一个颜色数据,第二个三角形有两个颜色数据
      let (color1, (color2_1, color2_2), color3) = (
        (1., 0., 0.),
        ((0., 0.8, 0.), (0., 0.5, 0.)),
        (0., 0., 1.),
      );
    
    • 创建相机view matrix需要的数据和projection matrix需要的数据

    view matrix需要eye、center、up这三个向量。在Main.re的_init函数中,创建它们:

      let ((eyeX, eyeY, eyeZ), (centerX, centerY, centerZ), (upX, upY, upZ)) = (
        (0., 0.0, 5.),
        (0., 0., (-100.)),
        (0., 1., 0.),
      );
    

    projection matrix需要near, far, fovy, aspect这四个值。在Main.re的_init函数中,创建它们:

      let (near, far, fovy, aspect) = (
        1.,
        100.,
        30.,
        (canvas##width |> Js.Int.toFloat) /. (canvas##height |> Js.Int.toFloat),
      );
    

    返回用于主循环的数据

    在Main.re的_init函数中,将WebGL上下文和场景数据返回,供主循环使用(只读):

    (
        gl,
        (
          (program1, program2),
          (indices1, indices2, indices3),
          (vertexBuffer1, indexBuffer1),
          (vertexBuffer2, indexBuffer2),
          (vertexBuffer3, indexBuffer3),
          (position1, position2, position3),
          (color1, (color2_1, color2_2), color3),
          (
            ((eyeX, eyeY, eyeZ), (centerX, centerY, centerZ), (upX, upY, upZ)),
            (near, far, fovy, aspect),
          ),
        ),
      )
    

    实现_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;
      });
    

    实现“_clearColor”

    接下来我们要实现_loopBody,它的伪代码为:

    type _loopBody = data => unit;
    let _loopBody = (data) => {
        data
        |> _clearColor
        |> _clearCanvas
        |> _render
    };
    

    我们首先实现_clearCanvas函数,为此需要在Gl.re中定义FFI:

    [@bs.send.pipe: webgl1Context]
    external clearColor: (float, float, float, float) => unit = "";
    

    然后在Main.re中实现_clearColor函数,设置清空颜色缓冲时的颜色值为黑色:

    let _clearColor = ((gl, sceneData) as data) => {
      WebGL1.clearColor(0., 0., 0., 1., gl);
    
      data;
    };
    

    实现“_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, sceneData) as data) => {
      WebGL1.clear(WebGL1.getColorBufferBit(gl) lor WebGL1.getDepthBufferBit(gl), gl);
    
      data;
    };
    

    实现“_render”

    _render的伪代码为:

    type _render = data => unit;
    let _render = (data) => {
        设置WebGL状态 
        |> 计算view matrix和projection matrix
        |> 计算三个三角形的model matrix
        |> 渲染三个三角形
    };
    

    下面分别实现:

    设置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状态,开启深度测试和背面剔除:

    WebGL1.enable(WebGL1.getDepthTest(gl), gl);
    
    WebGL1.enable(WebGL1.getCullFace(gl), gl);
    WebGL1.cullFace(WebGL1.getBack(gl), gl);
    

    计算view matrix和projection matrix

    计算这两个矩阵需要操作矩阵,而操作矩阵又需要操作向量,所以我们在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的_render函数中,传入数据,计算这两个矩阵:

      let vMatrix =
        Matrix.createIdentityMatrix()
        |> Matrix.setLookAt(
             (eyeX, eyeY, eyeZ),
             (centerX, centerY, centerZ),
             (upX, upY, upZ),
           );
      let pMatrix =
        Matrix.createIdentityMatrix()
        |> Matrix.buildPerspective((fovy, aspect, near, far));
    

    计算三个三角形的model matrix

    在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函数中,从三角形的位置数据中构造model matrix:

      let mMatrix1 =
        Matrix.createIdentityMatrix() |> Matrix.setTranslation(position1);
      let mMatrix2 =
        Matrix.createIdentityMatrix() |> Matrix.setTranslation(position2);
      let mMatrix3 =
        Matrix.createIdentityMatrix() |> Matrix.setTranslation(position3);
    

    渲染第一个三角形

    在_render函数中需要渲染三个三角形。
    我们来细化“渲染每个三角形”:

    渲染每个三角形 = 使用对应的Program |> 传递三角形的顶点数据 |> 传递view matrix和projection matrix |> 传递三角形的model matrix |> 传递三角形的颜色数据 |> 绘制三角形
    

    下面先绘制第一个三角形,分别实现它的子逻辑:

    • 使用对应的Program

    在Gl.re中定义FFI:

    [@bs.send.pipe: webgl1Context] external useProgram: program => unit = "";
    

    在Main.re的_render函数中使用program1:

    WebGL1.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 = WebGL1.getAttribLocation(program, "a_position", gl);
    
      positionLocation === (-1)
        ? error({j|Failed to get the storage location of a_position|j}) : ();
    
      WebGL1.bindBuffer(WebGL1.getArrayBuffer(gl), vertexBuffer, gl);
    
      WebGL1.vertexAttribPointer(
        positionLocation,
        3,
        WebGL1.getFloat(gl),
        false,
        0,
        0,
        gl,
      );
      WebGL1.enableVertexAttribArray(positionLocation, gl);
    };
    

    在Main.re的_render函数中调用sendAttributeData:

    Utils.sendAttributeData(vertexBuffer1, program1, gl);
    
    • 传递view matrix和projection matrix

    在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) =
      "";
    

    因为“传递view matrix和projection matrix”是通用逻辑,所以在Utils.re中增加sendCameraUniformData函数:
    首先判断program对应的GLSL中是否有view matrix对应的uniform:u_vMatrix和projection matrix对应的uniform:u_pMatrix;
    如果有,则传递对应的矩阵数据;否则,抛出错误信息。

    相关代码如下:

    let _unsafeGetUniformLocation = (program, name, gl) =>
      switch (WebGL1.getUniformLocation(program, name, gl)) {
      | pos when !Js.Null.test(pos) => Js.Null.getUnsafe(pos)
      | _ => error({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);
    
      WebGL1.uniformMatrix4fv(vMatrixLocation, false, vMatrix, gl);
      WebGL1.uniformMatrix4fv(pMatrixLocation, false, pMatrix, gl);
    };
    

    在Main.re的_render函数中调用sendCameraUniformData:

    Utils.sendCameraUniformData((vMatrix, pMatrix), program1, gl);
    
    • “传递三角形的model matrix”以及“传递三角形的颜色数据”

    在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) =>
      WebGL1.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);
    
      WebGL1.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);
    
      WebGL1.uniformMatrix4fv(mMatrixLocation, false, mMatrix, gl);
      _sendColorData(color1, gl, color1Location);
      _sendColorData(color2, gl, color2Location);
    };
    

    在Main.re的_render函数中调用sendModelUniformData1:

      Utils.sendModelUniformData1((mMatrix1, color1), 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绘制第一个三角形:

    WebGL1.bindBuffer(WebGL1.getElementArrayBuffer(gl), indexBuffer1, gl);
    
    WebGL1.drawElements(
        WebGL1.getTriangles(gl),
        indices1 |> Js.Typed_array.Uint16Array.length,
        WebGL1.getUnsignedShort(gl),
        0,
        gl,
    );
    

    渲染第二个和第三个三角形

    与渲染第一个三角形类似,在Main.re的_render函数中,使用对应的program,传递相同的相机数据,调用对应的Utils.sendModelUniformData1或sendModelUniformData2函数、绑定对应的VBO,来渲染第二个和第三个三角形。

    Main.re的_render函数的相关代码如下:

      //渲染第二个三角形
    
      WebGL1.useProgram(program2, gl);
    
      Utils.sendAttributeData(vertexBuffer2, program2, gl);
    
      Utils.sendCameraUniformData((vMatrix, pMatrix), program2, gl);
    
      Utils.sendModelUniformData2((mMatrix2, color2_1, color2_2), program2, gl);
    
      WebGL1.bindBuffer(WebGL1.getElementArrayBuffer(gl), indexBuffer2, gl);
    
      WebGL1.drawElements(
        WebGL1.getTriangles(gl),
        indices2 |> Js.Typed_array.Uint16Array.length,
        WebGL1.getUnsignedShort(gl),
        0,
        gl,
      );
      
      //渲染第三个三角形
    
      WebGL1.useProgram(program1, gl);
    
      Utils.sendAttributeData(vertexBuffer3, program1, gl);
    
      Utils.sendCameraUniformData((vMatrix, pMatrix), program1, gl);
    
      Utils.sendModelUniformData1((mMatrix3, color3), program1, gl);
    
      WebGL1.bindBuffer(WebGL1.getElementArrayBuffer(gl), indexBuffer3, gl);
    
      WebGL1.drawElements(
        WebGL1.getTriangles(gl),
        indices3 |> Js.Typed_array.Uint16Array.length,
        WebGL1.getUnsignedShort(gl),
        0,
        gl,
      );
    

    最终的项目结构图

    如下图所示:
    实现最小的3D程序-“绘制三角形”:最终领域模型 (1).png-75kB

    运行测试

    在浏览器中运行index.html页面,渲染结果如下图所示,其中测试场景包括三个三角形:
    此处输入图片的描述

    更新后的通用语言

    此处输入图片的描述

    总结

    本文通过需求分析、事件风暴、初步实现和具体实现,实现了最小的3D程序,渲染了三角形。

    本文成果

    我们通过本文的工作,获得了下面的成果:
    1、最小3D程序
    2、领域驱动设计的通用语言

    本文不足之处

    但是,还有很多不足之处:
    1、场景逻辑和WebGL API的调用逻辑混杂在一起
    2、存在重复代码:
    1)在_init函数的“初始化所有Shader”中有重复的模式
    2)在_render中,渲染三个三角形的代码非常相似
    3)Utils的sendModelUniformData1和sendModelUniformData2有重复的模式
    3、_init传递给主循环的数据过于复杂

    下文概要

    这些不足之处都是因为本文没有进行设计造成的。本文只建立了领域驱动设计的通用语言,重在实现,快速跑通了一个最小Demo。
    我们会在下文中按照领域驱动设计的思想进行设计,解决这些不足之处。

    本文完整代码地址

    Book-Demo-Triangle Github Repo

  • 相关阅读:
    面向对象的程序设计-继承
    Chrome开发工具之Console
    面向对象的程序设计-原型模式
    面向对象的程序设计-工厂模式、构造函数模式
    面向对象的程序设计-理解对象
    引用类型-Array类型
    引用类型-Object类型
    单体内置对象
    基本包装类型
    引用类型-Function类型
  • 原文地址:https://www.cnblogs.com/chaogex/p/12234673.html
Copyright © 2020-2023  润新知