研讨课 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
, texCoord
与indices
三个属性,但没有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的设计。