【计算机图形学】从0实现光线追踪 · 后篇

回顾

在上一节中,我们已经完成了:

  1. 架好了相机(Camera),摆放好场景(Scene);
  2. 射出射线、计算交点(IntersectResult);
  3. 实现了渲染器(Render),即将交点信息返回到画布(Canvas)上
     

接下来,我们将完成:

  1. 手写材质(Material),其中,材质最重要的方法是取样(sample),即获得颜色
  2. 手写光线追踪过程,这是一个递归
  3. 渲染出成品

 
 
 

颜色(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实现光线追踪 · 前篇

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值