回顾
在上一节中,我们已经完成了:
- 架好了相机(Camera),摆放好场景(Scene);
- 射出射线、计算交点(IntersectResult);
- 实现了渲染器(Render),即将交点信息返回到画布(Canvas)上
接下来,我们将完成:
- 手写材质(Material),其中,材质最重要的方法是取样(sample),即获得颜色
- 手写光线追踪过程,这是一个递归
- 渲染出成品
颜色(Color)
计算出交点处的颜色(color),返回并绘制到画布上,是我们的基本思路。
Color = function (r, g, b) {
this.r = r;
this.g = g;
this.b = b;
};
Color.prototype = {
copy: function () {
return new Color(this.r, this.g, this.b);
},
add: function (c) {
return new Color(this.r + c.r, this.g + c.g, this.b + c.b);
},
mul: function (s) {
return new Color(this.r * s, this.g * s, this.b * s);
},
mod: function (c) {
return new Color(this.r * c.r, this.g * c.g, this.b * c.b);
}
};
Color.black = new Color(0, 0, 0);
Color.white = new Color(1, 1, 1);
Color.red = new Color(1, 0, 0);
Color.green = new Color(0, 1, 0);
Color.blue = new Color(0, 0, 1);
Phong材质(PhongMaterial)
经典的Phong材质,是 漫反射+镜反射 的共同作用(这两个都是用公式算的,不要和递归时用的反射弄混了)。
其取样函数(sample⭐️)需要的参数有:入射光线ray、交点位置position、交点法线normal;光源方向lightDir、光源颜色lightColor —— 求出该交点处的颜色(color),正是取样函数的目的。
至于取样计算使用的数学公式和数学参数,不必关心。
PhongMaterial = function (diffuse, specular, shininess, reflectiveness) {
this.diffuse = diffuse; // 漫反射
this.specular = specular; // 镜反射
this.shininess = shininess;
this.reflectiveness = reflectiveness;
};
// 光源为平行光(方向+颜色,取样计算颜色时要用到)
var lightDir = new Vector3(1, 1, 1).normalize();
var lightColor = Color.white;
PhongMaterial.prototype = {
sample: function (ray, position, normal) {
var NdotL = normal.dot(lightDir);
var H = (lightDir.sub(ray.direction)).normalize();
var NdotH = normal.dot(H);
var diffuseTerm = this.diffuse.mul(Math.max(NdotL, 0));
var specularTerm = this.specular.mul(Math.pow(Math.max(NdotH, 0), this.shininess));
return lightColor.mod(diffuseTerm.add(specularTerm));
}
};
棋盘材质(CheckerMaterial)
取样函数(sample) 比较简单,只有黑色和白色。
CheckerMaterial = function (scale, reflectiveness) {
this.scale = scale;
this.reflectiveness = reflectiveness;
};
CheckerMaterial.prototype = {
sample: function (ray, position, normal) {
return Math.abs((Math.floor(position.x * 0.1) + Math.floor(position.z * this.scale)) % 2) < 1 ? Color.black : Color.white;
}
};
光线追踪(RayTrace)
代码思路:
function rayTraceRecursion(场景scene, 射线ray, 最大递归深度maxReflect):
计算交点 = scene.intersect(ray);
计算交点上的颜色 color;
计算反射光线 new_ray
交点上的颜色 = color + rayTraceRecursion(scene, new_ray, maxReflect - 1);
代码:
function rayTraceRecursive(scene, ray, maxReflect) {
var result = scene.intersect(ray);
if (result.geometry) {
var reflectiveness = result.geometry.material.reflectiveness;
var color = result.geometry.material.sample(ray, result.position, result.normal);
color = color.mul(1 - reflectiveness);
if (reflectiveness > 0 && maxReflect > 0) {
var r = result.normal.mul(-2 * result.normal.dot(ray.direction)).add(ray.direction);
ray = new Ray3(result.position, r);
var reflectedColor = rayTraceRecursive(scene, ray, maxReflect - 1);
color = color.add(reflectedColor.mul(reflectiveness));
}
return color;
} else {
return Color.black;
}
}
渲染器(Render)
和上一节中渲染器的写法完全相同,不再赘述。
只是,上一节的渲染器绘制的是返回来的深度/法向量等信息,这次绘制的是真正的颜色(color)。
function rayTraceReflection(canvas, scene, camera, maxReflect) {
var canvas = document.getElementById("canvas"); // 获取画布
var context = canvas.getContext("2d"); // 获取画布内容
var w = canvas.attributes.width.value;
var h = canvas.attributes.height.value;
context.fillStyle = "rgb(0, 0, 0)";
context.fillRect(0, 0, w, h);
var imageData = context.getImageData(0, 0, w, h);
var pixels = imageData.data;
scene.init();
camera.init();
var i = 0;
for (var y = 0; y < h; y++) {
var sy = 1 - y / h;
for (var x = 0; x < w; x++) {
var sx = x / w;
var ray = camera.generateRay(sx, sy);
var color = rayTraceRecursive(scene, ray, maxReflect);
pixels[i++] = color.r * 255;
pixels[i++] = color.g * 255;
pixels[i++] = color.b * 255;
pixels[i++] = 255;
}
}
context.putImageData(imageData, 0, 0);
}
最终效果展示
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>光线追踪(RayTracing)</title>
<script src="Vector3.js"></script>
<script src="Ray3.js"></script>
<script src="PerspectiveCamera.js"></script>
<script src="IntersectResult.js"></script>
<script src="Sphere.js"></script>
<script src="Plane.js"></script>
<script src="Scene.js"></script>
<script src="Render.js"></script>
<script src="Color.js"></script>
<script src="PhongMaterial.js"></script>
<script src="CheckerMaterial.js"></script>
<script src="RayTrace.js"></script>
</head>®
<body>
<canvas width="256" height="256" id="canvas"></canvas>
<script>
var plane = new Plane(new Vector3(0, 1, 0), 0);
var sphere1 = new Sphere(new Vector3(-10, 10, -10), 10);
var sphere2 = new Sphere(new Vector3(10, 10, -10), 10);
plane.material = new CheckerMaterial(0.1, 0.5);
sphere1.material = new PhongMaterial(Color.red, Color.white, 16, 0.25);
sphere2.material = new PhongMaterial(Color.blue, Color.white, 16, 0.25);
rayTraceReflection(
document.getElementById("canvas"),
new Scene([plane, sphere1, sphere2]),
new PerspectiveCamera(new Vector3(0, 5, 15), new Vector3(0, 0, -1), new Vector3(0, 1, 0), 90),
3);
</script>
</body>
</html>
>>> 这个演示有两个缺憾:
>>> 一是,光追是反射和折射的混合(要用到菲涅耳方程),但我们只递归了反射,并未递归折射
>>> 二是,光追的基本思路是“射出主射线primaryRay——命中后射出阴影线shadowRay”,我们没有考虑阴影线被遮挡的情况
上一篇:《【计算机图形学】从0实现光线追踪 · 前篇》