目录
3 unpack() 实现纹理转换 颜色值RGBA -> 深度值float
4 采样方法1 -> 泊松圆盘采样 Poisson-Disk Sampling
5 采样方法2 -> 均匀圆盘采样 Uniform-Disk Sampling
7 PCSS第二步 -> 半影估计,确定flter size
pack() 实现纹理转换 深度值float -> 颜色值RGBA
在做作业1之前,如果对框架有一个简单的了解,将有助于更好的完成作业!废话不多说,下面开始我的顺序简单解释一下框架里每一个文件都是干什么的:
indexl.html
关于html的学习可以看:GAMES202-熟悉HTML基本格式及代码标签(了解向)
这是一个html类型的文件,通过访问相对地址的方式引入代码框架的js文件,以及body部分的<canvas>标记创建了绘制图形,js文件可以用定义的id “glcanvas”访问这个canvas。
<body>
<!--<canvas>元素可以被用来通过JS脚本来实现图形的绘制,
支持全局属性,需要有闭合标签</canvas>-->
<canvas id="glcanvas"></canvas>
</body>
src -> engine.js
该文件中以关键字function定义了一个函数GAMES202Main(),程序载入后立即运行该方法,该方法获取了html中定义的<canvas>标签,并定义了其高宽属性,并创建了3D渲染的对象。
function GAMES202Main() {
// Init canvas and gl
// document文档节点(也叫根节点),可以访问整个HTML文档
// document.querySelector -> 获取文档中id为glcanvas的元素
// const声明的常量必须初始化,赋值过后不能再次修改
const canvas = document.querySelector('#glcanvas');
// 给当前图形标签添加高宽属性
canvas.width = window.screen.width;
canvas.height = window.screen.height;
// canvas标签的getContext()方法,这里表示:
// 创建一个WebGLRenderingContext对象作为3D渲染的上下文
const gl = canvas.getContext('webgl');
if (!gl) {
alert('Unable to initialize WebGL. Your browser or machine may not support it.');
return;
}
}
照相机 camera
关于Three.js中相机,可以看:Three.js学习-相机Camera的基本操作(了解向)
(1)在engine.js文件开头,就定义了用以方便改变相机位置的全局变量:
//var 一个全局变量 -> 相机位置
var cameraPosition = [30, 30, 30]
(2)之后在定义的GAMES202Main()函数中,对相机进行了定义:
//创建相机
//定义四个参数
const camera = new THREE.PerspectiveCamera(75, gl.canvas.clientWidth / gl.canvas.clientHeight, 1e-2, 1000);
//这里position直接调用最开始定的全局变量
camera.position.set(cameraPosition[0], cameraPosition[1], cameraPosition[2]);
轨道控制器 cameraControls
轨道控制器是OrbitControls.js这个文件,它是three.js的一个扩展控件,用以实现鼠标和场景的交互,涉及到的代码段有:
//OrbitControls.js -> 常用的好用的相机控制器,可以通过鼠标控制相机视野
// 创建轨道控制器
const cameraControls = new THREE.OrbitControls(camera, canvas);
//按键设置
cameraControls.enableZoom = true;//摄像机缩放
cameraControls.enableRotate = true;//摄像机旋转
cameraControls.enablePan = true;//摄像机平移
//一些速度设置
cameraControls.rotateSpeed = 0.3;
cameraControls.zoomSpeed = 1.0;
cameraControls.panSpeed = 0.8;
//由于设置了控制器,因此只能改变控制器的target以改变相机的lookAt方向
//默认为(0,0,0)
cameraControls.target.set(0, 0, 0);
渲染器 renderer
添加一个渲染器以实现WebGLRenderer.js
// Add renderer
const renderer = new WebGLRenderer(gl, camera);
光照 directionLight
three.js的光照分类可以看看:Three.js学习-光照和阴影(了解向)
engine.js里通过关键字new来创建DirectionalLight.js中定义的类对象,添加了一个方向光,并载入渲染器中。
// Add lights
// light - is open shadow map == true
let lightPos = [0, 80, 80];
let focalPoint = [0, 0, 0];
let lightUp = [0, 1, 0]
//添加方向光
const directionLight = new DirectionalLight(5000, [1, 1, 1], lightPos, focalPoint, lightUp, true, renderer.gl);
renderer.addLight(directionLight);
物体
先定义了用于比transform物体的三个变量(这里调用了最后面的一个方法setTransform()),并调用loadOBJ.js中定义的用来载入模型的函数loadOBJ(),将物体载入渲染器中。
// Add shapes
let floorTransform = setTransform(0, 0, -30, 4, 4, 4);
let obj1Transform = setTransform(0, 0, 0, 20, 20, 20);
let obj2Transform = setTransform(40, 0, -40, 10, 10, 10);
//将模型加入renderer中
loadOBJ(renderer, 'assets/mary/', 'Marry', 'PhongMaterial', obj1Transform);
loadOBJ(renderer, 'assets/mary/', 'Marry', 'PhongMaterial', obj2Transform);
loadOBJ(renderer, 'assets/floor/', 'floor', 'PhongMaterial', floorTransform);
...
//文件末尾定义的一个方法:
function setTransform(t_x, t_y, t_z, s_x, s_y, s_z) {
return {
modelTransX: t_x,
modelTransY: t_y,
modelTransZ: t_z,
modelScaleX: s_x,
modelScaleY: s_y,
modelScaleZ: s_z,
};
}
GUI
GAMES202Main()中还定义了一个实例化GUI的函数。原始的代码框架里并没有加入什么内容,我就加了一个自动旋转且能控制旋转速度的界面,代码如下:
//Add autoRotate
cameraControls.autoRotate = true;
cameraControls.autoRotateSpeed = 0.0;
//添加一个dat.GUI
function createGUI() {
//实例化GUI
const gui = new dat.gui.GUI();
//gui.add(); //直接添加GUI栏
const panelRotate = gui.addFolder('Model Move');
panelRotate.add(cameraControls, 'autoRotateSpeed', { Stopped: 0, Slow: 4, Fast: 10 });
panelRotate.open();
}
createGUI();
效果如下:
循环渲染
GAMES202Main()的最后,实现了循环渲染场景的操作。重点是requestAnimationFrame()。它是html提供的一个处理动画的API,()内会定义一个回调函数。它最大的好处就是不需要设置间隔时间,执行时机是由系统决定的,也就是屏幕刷新一次回调函数跟着执行一次,保证了动画的流畅。
//定义一个回调函数mainLoop()
function mainLoop(now) {
cameraControls.update();//每次递归更新一次控制器
renderer.render();//每次递归都执行一次渲染
//递归渲染
requestAnimationFrame(mainLoop);
}
//执行第一帧渲染
requestAnimationFrame(mainLoop);
src -> textures
由于后面都会涉及到framebuffer的内容,所以这里先介绍一下FBO这个类,首先介绍一下framebuffer这个概念。
FrameBuffer概念介绍
其实101老师就讲过,这里再复习一遍。
FrameBuffer也叫帧缓冲,即FBO。其实就是一段储存空间,可以直接理解成屏幕上显示内容对应的缓存,修改FrameBuffer的内容,在屏幕上就可以直接看到修改后对应的显示结果。FrameBuffer中缓存的内容可能包括:深度缓存、颜色缓存、模板缓存、累积缓存,因此FrameBuffer并不直接是屏幕上的像素,而是缓存内容共同作用下才能组成显示到屏幕上的图像效果。
后面实现Shadow Map时就需要用到framebuffer:为了对每个shading point实现2pass,可以先将场景中像素的深度值绘制到framebuffer中,形成深度缓存,在渲染时通过比较深度值来确定是否显示阴影即可。
FBO.js
这个js文件就是封装了上面介绍的FBO,在构造函数中创建了一个用以储存绘制的阴影的FBO,在DirectionalLights中创建了FBO,在之后会介绍到的shadowMaterial.js中将其传入其父类Material中,参与到后续的阴影绘制的工作中。
Texture.js
Buffer(数据缓冲区)和Texture(纹理)是WebGL程序的两大数据来源。Texture在很多情况下是通过image对象来构造的,这里我觉得只需要知道这个js文件是用来封装Texture的就行了。
src -> Materials
Material.js
定义一个基础材质
class Material {//声明一个class,定义一个Material类
//#表示私有字段,类似cpp里的private
#flatten_uniforms;
#flatten_attribs;
#vsSrc;
#fsSrc;
// Uniforms is a map(字典), attribs is a Array(数组)
//构造函数constructor()————创建和初始化在类中创建的对象
constructor(uniforms, attribs, vsSrc, fsSrc, frameBuffer) {
//构造函数()内有四个参数,
//用于设置以下字段this.xx的初始值
//这些this.xx字段表示创建一个实例字段名,其中2个公共字段2个私有
this.uniforms = uniforms;
this.attribs = attribs;
this.#vsSrc = vsSrc;
this.#fsSrc = fsSrc;
//材质定义一个#flatten_uniforms以保存uniforms的字段
this.#flatten_uniforms = ['uViewMatrix', 'uModelMatrix', 'uProjectionMatrix', 'uCameraPos', 'uLightPos'];
//用for in 的方式遍历数组,给#flatten_unifroms添加uniform的字段
for (let k in uniforms) {
this.#flatten_uniforms.push(k);
}
this.#flatten_attribs = attribs;
this.frameBuffer = frameBuffer;
}
//创建一个方法,用以储存mesh的额外属性
setMeshAttribs(extraAttribs) {
for (let i = 0; i < extraAttribs.length; i++) {
this.#flatten_attribs.push(extraAttribs[i]);
}
}
//创建一个方法,以实现调用Shader类以编译shader
compile(gl) {
return new Shader(gl, this.#vsSrc, this.#fsSrc,
{
uniforms: this.#flatten_uniforms,
attribs: this.#flatten_attribs
});
}
}
PhongMaterial.js
//通过extends关键字继承Material类
//Material->父类 PhongMaterial -> 子类
class PhongMaterial extends Material {
//构造函数传入了以下参数:
// vec3f color->材质颜色
// Texture colorMap->即材质的texture object-材质的纹理贴图
// vec3f specular->材质的高光项
// 类 light ->光源
// translate&scale -> 根据engine.js定义的setTransform()分别赋值
constructor(color, specular, light, translate, scale, vertexShader, fragmentShader) {
let lightMVP = light.CalcLightMVP(translate, scale);
let lightIntensity = light.mat.GetIntensity();
super({
// Phong
'uSampler': { type: 'texture', value: color },
'uKs': { type: '3fv', value: specular },
'uLightIntensity': { type: '3fv', value: lightIntensity },
// Shadow
'uShadowMap': { type: 'texture', value: light.fbo },
'uLightMVP': { type: 'matrix4fv', value: lightMVP },
}, [], vertexShader, fragmentShader);
}
}
作业1的PhongMaterial定义跟作业0的不同,相比作业0中构造函数给定的参数相比多了:
- translate:模型的参数
- scale:属于模型的参数
- light:属于光源的参数
这三个严格来说,应该是在实现渲染调用材质的同时才会赋予的光照和模型的参数。对于一个创建材质的类,应该只包括比如:颜色、高光项、顶点和片元着色器等,这里定义的材质已经属于Phong材质的实例化了。
后面还定义了一个异步函数buildPhongMaterial(),加上了对应的顶点和片元着色器,用以返回已经包括了着色器的类PhongMaterial。
ShadowMaterial.js
定义了一个继承Material基础材质的类ShadowMaterial,与Phong不同的是,再构造函数里还调用了light.fbo。
定义方向光类时就有:
this.fbo = new FBO(gl);//class FBO里创建了framebuffer
FBO类中:如果没有缓冲对象创建,则return null,frameBuffer为空。
在ShadowMaterial中加入这个参数并传入父类Material,可以用来在后续render()中调用的方法draw()中通过判断framebuffer是否为空决定是画在framebuffer还是屏幕上,以下:
if (this.material.frameBuffer != null) {
// Shadow map
//不为空 -> 画在framebuffer上
//resolution是在engine.js中定义的大小为2048的纹理分辨率
gl.viewport(0.0, 0.0, resolution, resolution);
} else {
//为空 -> 画在屏幕上
gl.viewport(0.0, 0.0, window.screen.width, window.screen.height);
}
src -> lights
DirectionalLight.js
定义了一个DirectionalLight类,类里包含了
- 一个构造函数(创建和初始化由类创建的对象)
//其中: lightPos是个齐次坐标,(x,y,z,w)中w=0 -> 无穷远(太阳光)
constructor(lightIntensity, lightColor, lightPos, focalPoint, lightUp, hasShadowMap, gl) {
this.mesh = Mesh.cube(setTransform(0, 0, 0, 0.2, 0.2, 0.2, 0));
this.mat = new EmissiveMaterial(lightIntensity, lightColor);
this.lightPos = lightPos; //光源
this.focalPoint = focalPoint; //焦点
this.lightUp = lightUp;
this.hasShadowMap = hasShadowMap;//方向光是有阴影的
this.fbo = new FBO(gl);//class FBO里创建了framebuffer
if (!this.fbo) {
console.log("无法设置帧缓冲区对象");
return;
}
}
- 一个CalcLightMVP()方法,该方法返回值为实现2pass shadow map的第一步render from light需要用到的MVP矩阵。
//计算光源方向看向场景的MVP矩阵
CalcLightMVP(translate, scale) {
let lightMVP = mat4.create();
let modelMatrix = mat4.create();
let viewMatrix = mat4.create();
let projectionMatrix = mat4.create();
// Model transform
// View transform
// Projection transform
mat4.multiply(lightMVP, projectionMatrix, viewMatrix);
mat4.multiply(lightMVP, lightMVP, modelMatrix);
return lightMVP;
}
与从照相机看向场景需要一个MVP变换矩阵相同,从光源看向场景也需要一个对应的矩阵,以实现场景坐标转换到光源处。这一步也是作业1需要完成任务之一。
Light.js
这里实现了作业1除了①Phong;②阴影;之外的第三个材质:EmissiveMaterial——自发光材质,同样它也继承了Material类。
- 自发光材质只有两个属性:光强intensity & 颜色color;
- 自发光材质当然也有属于自己的着色器:LightCubeVertexShader和LightCubeFragmentShader;
- light.js中还定义了方法GetIntensity(),返回的是三个方向颜色与intensity的乘积。
PointLight.js
定义了一个点光源的光照类,作业1中好像没有用到。
src -> renderers
WebGLRenderer.js
这里在render()方法中实现了Two Pass Shadow Map的方法,得到硬阴影。render()中实现了
- Shadow Pass —— Render from light -> 绘制深度图
- Camera Pass —— Render from eye(camera) -> 绘制到屏幕上
draw()
实现two pass之前先把光源draw出来,这就需要调用MeshRender中的方法draw()。
方法里用了gl.bindFramebuffer(target, framebuffer) —— 实现把framebuffer这个被绑定的帧缓冲区对象,绑定在target上。
draw中这个函数实现了:把"this.material.framebuffer"绑定给"gl.FRAMEBUFFER"上。
const gl = this.gl;
//语法gl.bindFramebuffer(target, framebuffer);
//target -> 指定绑定操作的缓冲区目标
//这里的target -> gl.FRAMEBUFFER -> 收集用于渲染图象的颜色、深度等数据储存
//framebuffer -> 要绑定的帧缓冲区对象的名字
gl.bindFramebuffer(gl.FRAMEBUFFER, this.material.frameBuffer);
下面贴出了我对WebGLRenderer.js文件的大概的理解,我认为只要知道render()函数实现了两个pass就行了。
//绘制入口
class WebGLRenderer {
meshes = [];
shadowMeshes = [];//用以储存shadow map
lights = [];
constructor(gl, camera) {
this.gl = gl;
this.camera = camera;
}
addLight(light) {
//给lights[]push一个具有2个属性entity和meshRender的对象
this.lights.push({
entity: light,
meshRender: new MeshRender(this.gl, light.mesh, light.mat)//(gl, mesh, material)
});
}
addMeshRender(mesh) { this.meshes.push(mesh); }
addShadowMeshRender(mesh) { this.shadowMeshes.push(mesh); }
render() {
const gl = this.gl;
//设置清屏等操作
gl.clearColor(0.0, 0.0, 0.0, 1.0); // Clear to black, fully opaque
gl.clearDepth(1.0); // Clear everything
gl.enable(gl.DEPTH_TEST); // Enable depth testing
gl.depthFunc(gl.LEQUAL); // Near things obscure far things
//这里不是很懂。。为什么!=0 -> 没光源 ==1 -> 有光源
console.assert(this.lights.length != 0, "No light");
console.assert(this.lights.length == 1, "Multiple lights");
for (let l = 0; l < this.lights.length; l++) {
// Draw light
// TODO: Support all kinds of transform
this.lights[l].meshRender.mesh.transform.translate = this.lights[l].entity.lightPos;
this.lights[l].meshRender.draw(this.camera);
// Shadow pass
//从光源生成一个shadow map储存(draw)到shadowMeshes[]中
if (this.lights[l].entity.hasShadowMap == true) {
for (let i = 0; i < this.shadowMeshes.length; i++) {
this.shadowMeshes[i].draw(this.camera);
}
}
// Camera pass
//调用着色器,把所有的网格mesh和阴影一起画出
for (let i = 0; i < this.meshes.length; i++) {
this.gl.useProgram(this.meshes[i].shader.program.glShaderProgram);
this.gl.uniform3fv(this.meshes[i].shader.program.uniforms.uLightPos, this.lights[l].entity.lightPos);
this.meshes[i].draw(this.camera);
}
}
}
}
src -> shaders
Shader.js
实现了着色器Shader从生成->编译compile再到程序对象从生成->编译的过程,详细的过程可以看我的另一篇文章:GAMES202-WebGL中shader的编译和连接(了解向)
这里再补充说一下类最后定义的一个方法。addShaderLocations(),这个方法在前面的构造函数也有使用:
constructor(gl, vsSrc, fsSrc, shaderLocations) {
this.gl = gl;
...
//绑定uniforms和attribs
this.program = this.addShaderLocations({
glShaderProgram: this.linkShader(vs, fs),
}, shaderLocations);
}
Shader被创建的时候是怎么赋值的呢?可以参考Material.js:
//创建了一个方法,以实现shader的编译
compile(gl) {
//这里给的是uniforms和attribs的值
return new Shader(gl, this.#vsSrc, this.#fsSrc,
{
uniforms: this.#flatten_uniforms,
attribs: this.#flatten_attribs
});
}
addShaderLocations()中出现的uniforms是js中的Map结构,跟c里的哈希表很相似,以键值对的形式(key -> value)来储存和访问数据。
- result:返回的值就是glShaderProgram的内容,绑定完成后可以实现linkShader()
- shaderLocations:还是可以参考上面的Material.js中对Shader的使用,就是用来储存uniforms和attribs
- uniforms和attribs分别对应getUniformLocation()和getAttribLocation()
lightShader&InternalShader.js
lightShader里的两个glsl着色器:
- LightCubeVertexShader.glsl
- LightCubeFragmentShaderglsl
实现了Light.js中定义的自发光材质的两个着色器,当然在作业1里这个自发光材质就是指的那个方向光了,之所以叫lightcube应该是在定义方向光时创建了一个cube以模拟灯的模型。
同时还需要注意,这两个属于light的glsl文件没有用,而是嵌入了InternalShader.js文件中进行使用。
phong -> phongVertex.glsl
内容与GAMES202-WebGL中shader的编译和连接(了解向)中作业0顶点着色器的部分内容差不多,但多了两个参数:
uniform mat4 uLightMVP;
varying highp vec4 vPositionFromLight;
...
vPositionFromLight = uLightMVP * vec4(aVertexPosition, 1.0);
- uLightMVP——光源方向的MVP矩阵
- vPositionFromLight——光源经过uLightMVP变换后的坐标
光源视角的vPositionFromLight和相机视角的gl_Position二者一起为后续实现2pass shadow map做准备。
phong -> phongFragment.glsl
Phong片元着色器中除了定义了一些参数,还定义了一些需要用到的函数:
1 float -> float 产生随机变量
// 从floatl类型的一维随机变量x产生一个[-1,1]范围的float
highp float rand_1to1(highp float x) {
// -1 -1
return fract(sin(x) * 10000.);
}
2 vec2 -> float 产生随机变量
highp float rand_2to1(vec2 uv) { // 从一个二维随机变量uv随机产生一个范围[0,1]的float变量
// 0 - 1
const highp float a = 12.9898, b = 78.233, c = 43758.5453;
highp float dt = dot(uv.xy, vec2(a, b)), sn = mod(dt, PI);
return fract(sin(sn) * c);
}
3 unpack() 实现纹理转换 颜色值RGBA -> 深度值float
//实现颜色值到深度值的映射
//这个unpack()可以看成是shadowFragment.glsl中的pack()的反转
// 将纹理的RGBA值转换成[0,1]的浮点数
float unpack(vec4 rgbaDepth) {
const vec4 bitShift =
vec4(1., 1. / 256., 1. / (256. * 256.), 1. / (256. * 256. * 256.));
return dot(rgbaDepth, bitShift);
}
那么这个函数怎么运用呢?我们知道,如果从光源处对场景采样,得到的将会是场景的颜色信息,在后面shadow map进行深度比较,需要用到unpack()将颜色值映射成可以进行比较的深度值。
同时,框架中提供了两种采样方法。
4 采样方法1 -> 泊松圆盘采样 Poisson-Disk Sampling
参考:三维点云泊松圆盘采样(Poisson-Disk Sampling)
泊松盘采样(Poisson Disk Sampling)生成均匀随机点 - HONT - 博客园 (cnblogs.com)
首先需要清楚的一点是,无论是通过泊松圆盘采样还是后面的均匀圆盘采样,我们通过采样方法都需要得到的是:在以下实现PCF和PCSS代码中
float PCF(sampler2D shadowMap, vec4 coords) { return 1.; }
float PCSS(sampler2D shadowMap, vec4 coords) {
// STEP 1: avgblocker depth
// STEP 2: penumbra size
// STEP 3: filtering
return 1.;
}
传入vec4类型的coords值(应该就是个齐次坐标(x, y, z, w))通过实现采样算法得到前面定义的一个数组
vec2 poissonDisk[NUM_SAMPLES];//NUM_SAMPLES = 20
得到的这个样本数组将参与PCF和PCSS后面的计算。
再回到我们当前的采样泊松圆盘采样算法:
由于人眼采样位置分布是不均匀的,研究者通过研究人眼中视锥细胞分布,并根据光感受器在猴子眼睛的中央窝外部的分布情况得到了泊松圆盘分布,并提出一种“泊松圆盘采样”的方法实现生成的随机点在面积上服从泊松圆盘分布。这种采样算法具有采样效果好,可以满足数据点均匀、保留细节部分情况较好的优点。
// 三维点云泊松圆盘采样
void poissonDiskSamples(const in vec2 randomSeed) {
float ANGLE_STEP = PI2 * float(NUM_RINGS) / float(NUM_SAMPLES);
float INV_NUM_SAMPLES = 1. / float(NUM_SAMPLES);
float angle = rand_2to1(randomSeed) * PI2;
float radius = INV_NUM_SAMPLES;
float radiusStep = radius;
for (int i = 0; i < NUM_SAMPLES; i++) {
poissonDisk[i] = vec2(cos(angle), sin(angle)) * pow(radius, .75);
radius += radiusStep;
angle += ANGLE_STEP;
}
}
5 采样方法2 -> 均匀圆盘采样 Uniform-Disk Sampling
就是实现:给定一个半径为r的圆,在圆内均匀产生n个采样点。
//均匀圆盘采样
void uniformDiskSamples(const in vec2 randomSeed) {
//根据传入的uv随机产生一个float
float randNum = rand_2to1(randomSeed);
//根据上面产生的float再生成一个[0,1]的float
float sampleX = rand_1to1(randNum);
float sampleY = rand_1to1(sampleX);
//这一部分实现了:
// θ = random(0,2Π)
// ρ = √θ
float angle = sampleX * PI2;
float radius = sqrt(sampleY);
for (int i = 0; i < NUM_SAMPLES; i++) {
//得到第i个采样点
// x = ρcosθ
// y = ρsinθ
poissonDisk[i] = vec2(radius * cos(angle), radius * sin(angle));
//继续进行随机采样过程
sampleX = rand_1to1(sampleY);
sampleY = rand_1to1(sampleX);
angle = sampleX * PI2;
radius = sqrt(sampleY);
}
}
上面是极坐标法,如果从直角坐标其实可以先从[-1,1]²正方体中随机取点(x,y),再仅保留在半径为1的圆内的点就可以。
我们已经可以预测:均匀圆盘采样方法相比较于泊松圆盘采样误差会更大,因为泊松圆盘分布才更接近人眼,后续在做作业1时可以对比看看。
6 PCSS第一步 -> 寻找遮挡物,计算遮挡物平均深度
这一步是fndBlocker()这个函数中完成的,他实现了PCSS的第一个步骤:
- Blocker Serach -> 记录所有的bloker深度,取均值得到需要的blocker depth;
它也是作业1实现PCSS需要补充的函数之一。
float findBlocker(sampler2D shadowMap, vec2 uv, float zReceiver) { return 1.; }
7 PCSS第二步 -> 半影估计,确定flter size
这一步是在PCSS()函数中实现的,这里也是作业1实现PCSS需要补充的函数之一。所谓的半影估计就是课上老师介绍的那个相似三角形中这个半影范围的计算。
float PCSS(sampler2D shadowMap, vec4 coords) {
// STEP 1: avgblocker depth
// STEP 2: penumbra size
// STEP 3: filtering
return 1.;
}
8 实现PCF,也是PCSS的第三步
这一步在PCF()函数中实现,这也是作业1需要补充的函数之一。
float PCF(sampler2D shadowMap, vec4 coords) { return 1.; }
9 实现Shadow Map
这一步是在useShadowMap()中实现的,也是作业1需要补充的函数之一。
float useShadowMap(sampler2D shadowMap, vec4 shadowCoord) { return 1.; }
10 实现BlinnPhong光照模型
就是实现Blinn-Phong光照模型,并返回经过伽马矫正后的值,具体过程注释可以参考我的这篇文章:GAMES202-WebGL中shader的编译和连接(了解向)_flashinggg的博客-CSDN博客
11 着色器的主函数,实现阴影对着色的影响
其实就是将作业1中做的三种阴影的计算实现出来,想要实现哪个就把//注释去掉就行。
//应用shadowmap/PCF/PCSS
void main(void) {
// 归一化
vec3 shadowCoord = vPositionFromLight.xyz / vPositionFromLight.w;
float visibility;
// visibility = useShadowMap(uShadowMap, vec4(shadowCoord, 1.0));
// visibility = PCF(uShadowMap, vec4(shadowCoord, 1.0));
// visibility = PCSS(uShadowMap, vec4(shadowCoord, 1.0));
vec3 phongColor = blinnPhong();
// gl_FragColor = vec4(phongColor * visibility, 1.0);
gl_FragColor = vec4(phongColor, 1.);
}
shadowShader
这里阴影着色器定义的参数跟phongShader差不多,但是值得注意的是:
pack() 实现纹理转换 深度值float -> 颜色值RGBA
这个函数跟phongFragment里的unpack()实现的操作是反着来的。
//实现深度向颜色值的映射
//将一个[0,1]的浮点数转换成RGBA值并返回
vec4 pack (float depth) {
// 使用rgba 4字节共32位来存储z值,1个字节精度为1/256
const vec4 bitShift = vec4(1.0, 256.0, 256.0 * 256.0, 256.0 * 256.0 * 256.0);
const vec4 bitMask = vec4(1.0/256.0, 1.0/256.0, 1.0/256.0, 0.0);
// gl_FragCoord:片元的坐标,fract():返回数值的小数部分
vec4 rgbaDepth = fract(depth * bitShift); //计算每个点的z值
rgbaDepth -= rgbaDepth.gbaa * bitMask; // Cut off the value which do not fit in 8 bits
return rgbaDepth;
}
src -> loads
loadOBJ.js
这里主要参考了这篇文章里对loadOBJ的理解:GAMES202 作业1框架代码结构分析 - 知乎 (zhihu.com)
let material, shadowMaterial;
为模型分别创建了material和shadowMaterial
并分别关联了以下对象:
- 模型 与 Shader
- phongVertex.glsl和phongFragment.glsl 与 模型的material
- shadowFragment.glsl和shadowVertex.glsl 与 模型的shadowMaterial
终于总结完了!!这些只是我不太全面的梳理,希望能帮助到你。