Vigilans/TypeGL

研讨课 4

Closed this issue · 3 comments

主题:光照和明暗绘制

  • 掌握光照和明暗绘制的基本原理、设置步骤,并讨论实验效果与参数设置的关系 
  • 场景中加入光照,要求:
    • 光源位置可交互改变
    • 场景中至少包含两种以上的材质
    • 附加要求:在场景中绘制出光源

Todo List:

  • 画平面的API -- yzh
  • 画球的API(我们到现在才做。。。) -- yzh
  • 为光源这种没有朝向、法向量的Object定义一种新Controller -- yzh
  • 实现光照效果 -- lsc
  • 让光源绕z轴转动,实现光照和阴影的动态效果 -- yzh
  • Optional: 修复那个贝塞尔鱼的bug -- yzh
  • Optional: 调教好Camera -- yzh
  • Optional: 实现那个高级版阴影逻辑(留给下一次了)

3D图形模型

与2D图形不同,3D图形的除了顶点坐标以外,还需要法向量信息。此外,由于3D图形更接近”模型“的概念,因此其每个顶点往往还需要附带材质坐标,以及通过索引使用各个顶点。

因此,总结下来,一个3D模型需要的Attributes有:

{
    position: { numComponents: 3, data: positions },
    texCoord: { numComponents: 2, data: texCoords },
    normal:   { numComponents: 3, data: normals },
    indices:  { numComponents: 3, data: indices },
}

从而我们可以为每种3D图形定义一个create{XXX}Vertices函数,其返回的顶点信息即为上面的内容。

目前项目支持的3D图形有:

  • createPlaneVertices
  • createSphereVertices
  • createCubeVertices

有了顶点创建函数后,其实也有了绘制函数。实际上,每种3D图形的绘制方法大同小异,我们需要知道的信息大概有:

  • 图形的颜色
  • 绘图模式("fill" | "stroke"
  • 着色器源(可选)
  • 绘图位置(可选)
  • 图形的顶点Attributes集合

因此,我们可以建立一个bindDrawing3D函数,它接受一个createXxVertices函数,将其包装后返回一个drawXX函数,由此获得了一系列绘图函数。2D图形实际上也可通过这种方式处理。

2D to 3D

一个2D图形可以通过旋转操作形成一个3D模型,这个过程称为Lathe。

因此,我们可以提供一个lathePoint函数,将2D图形转化成一个3D模型。

function lathePoints(
    points: Array<MV.Vector2D>,
    scale: MV.Vector3D = [1, 1, 1], // 放缩倍数
    axis: MV.Vector3D = [0, 1, 0],  // 旋转轴
    startAngle = 0,         // 起始角 (例如 0)
    endAngle = Math.PI * 2, // 终止角 (例如 Math.PI * 2)
    numDivisions = 8,       // 这中间生成多少块
    capStart = true,        // true 就封闭起点
    capEnd = true           // true 就封闭终点
) : WebGLAttributeMap;

这个函数创建出来的Model拥有position, texCoordindices三个属性,但没有normal属性。我们还需要一个generateNormals函数,为模型生成法线:

function generateNormals(model: WebGLAttributeMap, maxAng: number) : WebGLAttributeMap;

需要注意的是,一个位于棱角上的顶点,由于周围梯度的不连续,会同时具有多个法线。这时候一般的措施是生成多个坐标相同,但法线不一样的顶点。因此,generateNormals函数生成的Model的顶点数一般是会增加的。

Controller

虽然Controller机制从第二次研讨开始就存在,但直到这一次才算是正式定型。

主要功能

Controller是Core的一个主要模块,是对键盘交互机制的封装。

Controller绑定于一个WebGL渲染对象,并保存了键盘控制对这个对象的累积效应。

通过键盘上的数字键,可以切换当前监听的Controller。

当绑定的对象的种类不同时,其键盘交互规则也会有所不同。具体为:

  • 若一个对象是有向的(即属于WebGLOrientedObject),则
    • Q/E: 控制缩放
    • W/S: 沿当前朝向前进/后退
    • A/D: 沿法线旋转(转头)
    • ←/→: 沿当前朝向旋转(侧翻)
    • ↑/↓: 沿侧线旋转(抬头/低头)
  • 否则,由于其没有方向的概念,则对其进行简单的坐标控制:
    • Q/E: 控制缩放
    • ←/→: 沿X轴移动
    • ↑/↓: 沿Y轴移动
    • W/S: 沿Z轴移动
    • A/D: 沿Y轴旋转

实现算法

Controller

Controller的数据结构比较简单:

  • obj: 被绑定的对象
  • T/R/S: 平移/旋转/缩放矩阵的累积
  • speed: 变化速度
  • 提供了一个虚函数update(event: KeyBoardEvent),用于更新以上矩阵。

可以看到,Controller本身只记录信息,其并不改变外部的状态。

Controller对自己信息的修改,也是借助外部的键盘信息完成的:

Controller的子类实现update函数,为KeyBoardEvent添加自定义解析逻辑,进而通过该函数来完成状态更新。可以看到,这个过程中Controller也没有主动索取外部的状态,而是等待外部的调用。

bindController

真正将Controller、键盘设备、渲染管道联系起来的,是Canvas.bindController函数。

该函数首先接受一系列初始化参数,并设定默认值,用于设置Controller的初状态:

init = Object.assign({
    offset: [0, 0, 0],
    scale: 1.0,
    rotate: [0, 0, 0],
    speed: 1.0
}, init); // 设置 init 的默认值

之后,检查这是否是第一次设置Controller(通过检查Canvas.controllers属性是否存在来实现)。若为第一次设置,则该函数会为Canvas对象(this)绑上几个新属性:

this.controllers = [];
this.ctrlPointer = 0;
Object.defineProperty(this, "curCtrl", {
    get: () => this.controllers[this.ctrlPointer]
}); // 该getter用于获取当前监听的Controller

然后,将更新逻辑推送进渲染管线的更新管道中:

  • 找到当前监听的Controller,将其绑定的对象的worldMatrix直接设为T*R*S(累积效应)。
  • 第一次更新时,需要把每个Controller的初状态应用到对象上,因此对所有Controller进行一次更新。

最后,设置键盘监听的逻辑:

  • 如果按下的是数字键,则切换当前监听的Controller(即修改ctrlPointer)。
  • 对于其他情况,则转发至当前监听的Controller的update函数,多态调用对应种类的Controller处理。

最后的最后,不管是否是第一次设置,我们都生成一个新的Controller绑定到对象上:

  • 如果对象是有朝向概念的,则绑定OrientedController
  • 否则,绑定DefaultController(方块、光源等都落在这里)

至此,便完成了Controller的设计。