光线追踪入门,计算机图形学

原标题:总结机图形学——光线追踪(RayTracing)算法

图片 1

图片 2

本文是对 赵明先生
《总括机图形学》MOOC课程
有的章节的小计算。

壹、理论基础

文山会海简介

纪念时辰候读过一本有关电脑图形学(computer graphics,
CG)的入门书,从此就爱上了CG。本种类希望,采纳很多少人认识的JavaScript语言去享受CG,令越来越多个人有空子接触,并爱上CG。

本体系的性状之壹,是读者能在浏览器里平素实施代码,也可重覆修改代码测试。透过那种相互,可能能更深入体会内容。读者只要理解JavaScript(因为JavaScript很简单,学过Java/C/C++/C#等等的言语也应没难题)和一点点线性代数(linear
algebra)就能够了。

小编在高校时期并从未修读CG课程,即便看过有关书籍,始终未亲手做过全域光照的渲染器,本文也作为个人的学习分享。其它,小编也差不离十年没接触JavaScript,希望各位不吝赐教。

连串简介

记得儿时读过壹本有关电脑图形学(computer graphics,
CG)的入门书,从此就爱上了CG。本种类希望,选择很多个人认识的JavaScript语言去分享CG,令更四个人有机遇接触,并爱上CG。

本体系的本性之一,是读者能在浏览器里一向执行代码,也可重覆修改代码测试。透过那种互动,也许能更深远回味内容。读者只要精晓JavaScript(因为JavaScript很简短,学过Java/C/C++/C#等等的语言也应没难点)和一丝丝线性代数(linear
algebra)就足以了。

小编在高等高校之间并未有修读CG课程,就算看过相关书籍,始终未亲手做过全域光照的渲染器,本文也视作个体的学习分享。其它,小编也大多十年没接触JavaScript,希望各位不吝赐教。

 

一、空间维度场景中创设图像

正文简介

大部程序员听到3D
CG,就会联想到Direct3D、OpenGL等API。事实上,那几个流行的API重要为实时渲染(real-time
rendering)而设,一般接纳光栅化(rasterization)情势,渲染多量的三角(或任何几何图元种类(primitive
types))。那种根据光栅化的渲染系统,只接济部分光照(local
illumination)。换句话说,渲染几何图形的3个像素时,光照总括只好获得该像素的音信,而不能够访问其余几何图形资源信息。理论上,阴影(shadow)、反射(reflection)、折射(refraction)等为大局光照(global
illumination)效果,实际上,栅格化渲染系统能够动用预处理(如阴影贴图(shadow
mapping)、环境贴图(environment mapping))去模拟那几个效能。

大局光照计算量大,①般也并未有异样硬件加快(平日只使用CPU而非GPU),所以只适合离线渲染(offline
rendering),例如3D Studio
马克斯、Maya等工具。在那之中三个匡助全局光照的点子,称为光线追踪(ray
tracing)。光线追踪能不难间接地支持阴影、反射、折射,实现起来亦卓殊不难。本文的例证里,只用了数十行JavaScript代码(除canvas外不需求别的出色插件和库),就能兑现1个支撑反射的光明追踪渲染器。光线追踪能够用来上学很多总结机图形学的课题,也许比学习Direct3D/OpenGL更易于。以后,先介绍点理论吧。

本文简介

多数程序员听到3D
CG,就会联想到Direct3D、OpenGL等API。事实上,这几个流行的API主要为实时渲染(real-time
rendering)而设,1般采纳光栅化(rasterization)情势,渲染大量的三角形(或其余几何图元系列(primitive
types))。那种基于光栅化的渲染系统,只接济部分光照(local
illumination)。换句话说,渲染几何图形的二个像素时,光照总结只好取得该像素的情报,而不能够访问其余几何图形资源音信。理论上,阴影(shadow)、反射(reflection)、折射(refraction)等为大局光照(global
illumination)效果,实际上,栅格化渲染系统能够运用预处理(如阴影贴图(shadow
mapping)、环境贴图(environment mapping))去模拟那一个意义。

大局光照总结量大,一般也未尝格外硬件加快(平时只利用CPU而非GPU),所以只适合离线渲染(offline
rendering),例如3D Studio
马克斯、Maya等工具。当中八个帮助全局光照的法门,称为光线追踪(ray
tracing)。光线追踪能简单直接地援助阴影、反射、折射,完成起来亦十分不难。本文的事例里,只用了数十行JavaScript代码(除canvas外不要求任何极度插件和库),就能促成1个协理反射的光华追踪渲染器。光线追踪能够用来读书很多电脑图形学的课题,或然比上学Direct3D/OpenGL更易于。以后,先介绍点理论吧。

直线是结合图形的底子,其算法往往被1再调用,其好坏直接影响图形的来得效果和进程。以下是1些画直线的常用算法。

首先步:透视投影。那是二个将三个维度物体的造型投影到图像表面上的几何进程,这一步只必要连接从指标特征到肉眼里面包车型大巴线,然后在画布上制图那几个投影线与图像平交的概略。

光线追踪

光栅化渲染,不难地说,正是把大气三角形画到显示器上。当中会接纳深度缓冲(depth
buffer,
z-buffer),来缓解四个三角形重叠时的内外难题。三角形数目影响效应,但三角形在显示器上的总面积才是任重(英文名:rèn zhòng)而道远瓶颈。

光线追踪,不难地说,就是从壁画机的职务,通过影像平面上的像素地方(比较不错的布道是取样(sampling)地点),发射一束光线插手景,求光线和几何图形间方今的交点,再求该交点的著色。若是该交点的质地是反射性的,能够在该交点向反射方向继续追踪。光线追踪除了简单扶助部分大局光照效果外,亦不局限于三角形作为几何图形的单位。任何几何图形,能与一束光线总括交点(intersection
point),就能支撑。

图片 3

上海教室(來源)展现了光明追踪的中坚情势。要总括一点是或不是在影子之内,也只须发射1束光线到光源,检查实验中间有未有障碍物而已。可是光源和影子留待下回分解。

光线追踪

光栅化渲染,简单地说,便是把大气三角形画到显示屏上。个中会利用深度缓冲(depth
buffer,
z-buffer),来消除四个三角形重叠时的光景难题。三角形数目影响成效,但三角形在显示屏上的总面积才是重大瓶颈。

光线追踪,简单地说,就是从油画机的地方,通过影像平面上的像素地方(比较不利的说法是取样(sampling)位置),发射1束光线参预景,求光线和几何图形间方今的交点,再求该交点的著色。即便该交点的材质是反射性的,可以在该交点向反射方向接续追踪。光线追踪除了简单援助部分大局光照效果外,亦不局限于三角形作为几何图形的单位。任何几何图形,能与一束光线统计交点(intersection
point),就能支持。

图片 4

上图(來源)展现了光辉追踪的骨干方法。要总计一点是还是不是在阴影之内,也只须发射壹束光线到光源,检验中间有未有障碍物而已。然而光源和阴影留待下回分解。

 

第一步:添加颜色。图像概况绘制好之后,给它的骨子增进颜色,那样就大功告成了三个维度场景中的图像成立进度。

初试画板

光明追踪的输出只是1个形象(image),所谓印象,就是二维颜色数组。

要在浏览器内,用JavaScript生成多个形象,如今能够使用HTML
5的<canvas>。但近年来Internet
Explorer(直至版本八)还不援助<canvas>,别的浏览器如Chrome、Firefox、Opera等就足以。

以下是1个粗略的试行,把种种象素填入颜色,左至右越来越红,上至下愈加绿。

var canvas = document.getElementById(“testCanvas”); var ctx =
canvas.getContext(“2d”); var w = canvas.attributes.width.value; var h =
canvas.attributes.height.value; ctx.fillStyle = “rgb(0,0,0)”;
ctx.fillRect(0, 0, w, h); var imgdata = ctx.getImageData(0, 0, w, h);
var pixels = imgdata.data; var i = 0; for (var y = 0; y < h; y++) for
(var x = 0; x < w; x++) { pixels[i++] = x / w * 255; pixels[i++]
= y / h * 255; pixels[i++] = 0; pixels[i++] = 255; }
ctx.putImageData(imgdata, 0, 0);

Run

 

左邊的canvas定義如下:

<canvas width="256" height="256" id="testCanvas"></canvas>

修改代码试试看

  • 把第三个pixels[i++] = 0 改为255 (即蓝色全开)
  • 把第四个pixels[i++] = 255 改为128 (alpha=128)
  • 可以只修改两个for循环里面的代码,画一个国际象棋棋盘么?

光线追踪入门,计算机图形学。那实验注明,从canvas取得的印象质感canvas.getImageData(…).data是个壹维数组,该数组每四个因素代表一个象素(按红,
绿, 蓝, 阿尔法排列),那几个象素在形象中从上至下、左至右排列。

缓解实验平台的技术难题后,可起首从基础项目开端兑现。

初试画板

光明追踪的出口只是二个形象(image),所谓影象,就是二维颜色数组。

要在浏览器内,用JavaScript生成2个影象,近年来得以采纳HTML
五的<canvas>。但今后Internet
Explorer(直至版本捌)还不帮忙<canvas>,别的浏览器如Chrome、Firefox、Opera等就能够。

以下是二个归纳的尝试,把各类象素填入颜色,左至右越来越红,上至下越来越绿。

var canvas = document.getElementById(“testCanvas”); var ctx =
canvas.getContext(“2d”); var w = canvas.attributes.width.value; var h =
canvas.attributes.height.value; ctx.fillStyle = “rgb(0,0,0)”;
ctx.fillRect(0, 0, w, h); var imgdata = ctx.getImageData(0, 0, w, h);
var pixels = imgdata.data; var i = 0; for (var y = 0; y < h; y++) for
(var x = 0; x < w; x++) { pixels[i++] = x / w * 255; pixels[i++]
= y / h * 255; pixels[i++] = 0; pixels[i++] = 255; }
ctx.putImageData(imgdata, 0, 0);

Run

 

左邊的canvas定義如下:

<canvas width="256" height="256" id="testCanvas"></canvas>

修改代码试试看

  • 把第三个pixels[i++] = 0 改为255 (即蓝色全开)
  • 把第四个pixels[i++] = 255 改为128 (alpha=128)
  • 可以只修改两个for循环里面的代码,画一个国际象棋棋盘么?

那实验证明,从canvas取得的形象资料canvas.getImageData(…).data是个一维数组,该数组每三个要素代表2个象素(按红,
绿, 蓝, 阿尔法排列),那一个象素在形象中从上至下、左至右排列。

化解实验平台的技艺难点后,可起先从基础项目开头落到实处。

1、DDA算法:

图片 5

基础类

小编使用基于物件(object-based)的法子编写JavaScript。

基础类

作者利用基于物件(object-based)的主意编写JavaScript。

  此算法基于增量思想。

图片 6

三个维度向量

三个维度向量(3D
vector)可谓CG里最常用型别了。那里三个维度向量用Vector三类达成,用(x, y,
z)表示。 Vector叁亦用来表示空间中的点(point),而不另建类。先看代码:

Vector3 = function(x, y, z) { this.x = x; this.y = y; this.z = z; };

Vector3.prototype = {
    copy : function() { return new Vector3(this.x, this.y, this.z); },
    length : function() { return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); },
    sqrLength : function() { return this.x * this.x + this.y * this.y + this.z * this.z; },
    normalize : function() { var inv = 1/this.length(); return new Vector3(this.x * inv, this.y * inv, this.z * inv); },
    negate : function() { return new Vector3(-this.x, -this.y, -this.z); },
    add : function(v) { return new Vector3(this.x + v.x, this.y + v.y, this.z + v.z); },
    subtract : function(v) { return new Vector3(this.x - v.x, this.y - v.y, this.z - v.z); },
    multiply : function(f) { return new Vector3(this.x * f, this.y * f, this.z * f); },
    divide : function(f) { var invf = 1/f; return new Vector3(this.x * invf, this.y * invf, this.z * invf); },
    dot : function(v) { return this.x * v.x + this.y * v.y + this.z * v.z; },
    cross : function(v) { return new Vector3(-this.z * v.y + this.y * v.z, this.z * v.x - this.x * v.z, -this.y * v.x + this.x * v.y); }
};

Vector3.zero = new Vector3(0, 0, 0);

这一个类格局(如normalize、negate、add等),即便传回Vector3类对象,都会传出三个新建构的Vector叁。那几个三个维度向量的效应很简短,不在此详述。注意multiply和divide是与纯量(scalar)相乘和相除。

Vector叁.zero用作常量,幸免每一遍重复构建。值得一提,那些常量必需在prototype设定之后才能定义。

三个维度向量

三个维度向量(3D
vector)可谓CG里最常用型别了。那里三个维度向量用Vector3类达成,用(x, y,
z)表示。 Vector3亦用来代表空间中的点(point),而不另建类。先看代码:

Vector3 = function(x, y, z) { this.x = x; this.y = y; this.z = z; };

Vector3.prototype = {
    copy : function() { return new Vector3(this.x, this.y, this.z); },
    length : function() { return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); },
    sqrLength : function() { return this.x * this.x + this.y * this.y + this.z * this.z; },
    normalize : function() { var inv = 1/this.length(); return new Vector3(this.x * inv, this.y * inv, this.z * inv); },
    negate : function() { return new Vector3(-this.x, -this.y, -this.z); },
    add : function(v) { return new Vector3(this.x + v.x, this.y + v.y, this.z + v.z); },
    subtract : function(v) { return new Vector3(this.x - v.x, this.y - v.y, this.z - v.z); },
    multiply : function(f) { return new Vector3(this.x * f, this.y * f, this.z * f); },
    divide : function(f) { var invf = 1/f; return new Vector3(this.x * invf, this.y * invf, this.z * invf); },
    dot : function(v) { return this.x * v.x + this.y * v.y + this.z * v.z; },
    cross : function(v) { return new Vector3(-this.z * v.y + this.y * v.z, this.z * v.x - this.x * v.z, -this.y * v.x + this.x * v.y); }
};

Vector3.zero = new Vector3(0, 0, 0);

这几个类形式(如normalize、negate、add等),假使传回Vector3类对象,都会传播二个新建构的Vector叁。那几个三个维度向量的功效不会细小略,不在此详述。注意multiply和divide是与纯量(scalar)相乘和相除。

Vector三.zero用作常量,防止每一遍重复构建。值得一提,那一个常量必需在prototype设定之后才能定义。

  对于直线的斜截式:y=kx+b,考虑每趟 x 递增 一, 都有 y[i+1] = y[i]

二、物体的水彩和亮度

光线

所谓光线(ray),从一些向某方向发射也。数学上可用参数函数(parametric
function)表示:

\mathbf{r}(t) = \mathbf{o} + t\mathbf{d}, t \geq 0

高级中学档,o即发谢起源(origin),d为主旋律。在本文的例子里,都即使d为单位向量(unit
vector),由此t为距离。达成如下:

Ray3 = function(origin, direction) { this.origin = origin; this.direction = direction; }

Ray3.prototype = {
    getPoint : function(t) { return this.origin.add(this.direction.multiply(t)); }
};

光线

所谓光线(ray),从1些向某方向发射也。数学上可用参数函数(parametric
function)表示:

\mathbf{r}(t) = \mathbf{o} + t\mathbf{d}, t \geq 0

中级,o即发谢起点(origin),d为主旋律。在本文的事例里,都就算d为单位向量(unit
vector),由此t为离开。完结如下:

Ray3 = function(origin, direction) { this.origin = origin; this.direction = direction; }

Ray3.prototype = {
    getPoint : function(t) { return this.origin.add(this.direction.multiply(t)); }
};
  • k,那样就将 kx 部分的乘法转换成递推的加法。


首即便光明与实体材料互相成效的结果。

球体

圆球(sphere)是里面3个最不难易行的立体几何图形。那里只考虑球体的表面(苹果平板),中央点为c、半径为r的圆球表面可用等式(equation)表示:

\left \| \mathbf{x} – \mathbf{c} \right \| = r

如前文所述,供给总结光线和球体的近年交点。只要把光芒x =
r(t)代入球体等式,把该等式求解正是交点。为简化方程,设v=o – c,则:

\begin{align*} \left\| \mathbf{v} +t\mathbf{d} \right\| &= r
\\ \left\| \mathbf{v} +t\mathbf{d} \right\|^2 &= r^2 \\
(\mathbf{v}+t\mathbf{d}) \cdot (\mathbf{v}+t\mathbf{d}) – r^2 &= 0
\\ \mathbf{v}^2 + 2t(\mathbf{d} \cdot \mathbf{v}) +t^2
\mathbf{d}^2 – r^2 &= 0 \\ (\mathbf{d}^2)t^2 + (2\mathbf{d} \cdot
\mathbf{v})t + (\mathbf{v}^2 – r^2) &= 0 \end{align*}

因为d为单位向量,所以2回方的全面能够消去。 t的1遍方程式的解为

\begin{align*} t&=\frac{-2\mathbf{d} \cdot \mathbf{v}\pm
\sqrt{(2\mathbf{d} \cdot \mathbf{v})^2 – 4(\mathbf{v}^ 2 – r^2)}
}{2} \\ &= -\mathbf{d} \cdot \mathbf{v}\pm \sqrt{(\mathbf{d}
\cdot \mathbf{v})^2 – (\mathbf {v}^2 – r^2)} \end{align*}

若根号内为负数,即相交不发生。其它,由于此地只必要取近来的交点,由此正负号只需取负号。代码达成如下:

Sphere = function(center, radius) { this.center = center; this.radius = radius; };

Sphere.prototype = {
    copy : function() { return new Sphere(this.center.copy(), this.radius.copy()); },

    initialize : function() {
        this.sqrRadius = this.radius * this.radius;
    },

    intersect : function(ray) {
        var v = ray.origin.subtract(this.center);
        var a0 = v.sqrLength() - this.sqrRadius;
        var DdotV = ray.direction.dot(v);

        if (DdotV <= 0) {
            var discr = DdotV * DdotV - a0;
            if (discr >= 0) {
                var result = new IntersectResult();
                result.geometry = this;
                result.distance = -DdotV - Math.sqrt(discr);
                result.position = ray.getPoint(result.distance);
                result.normal = result.position.subtract(this.center).normalize();
                return result;
            }
        }

        return IntersectResult.noHit;
    }
};

落到实处代码时,尽快用最少的演算剔除没相交的动静(Math.sqrt是相比较慢的函数)。另外,预总计了球体半径r的平方,此为3个优化。

那里用到多少个IntersectResult类,这一个类只用来记录交点的几何物件(geometry)、距离(distance)、位置(position)和法向量(normal)。
IntersectResult.noHit的geometry为null,代表光线未有和任何几何物件相交。

IntersectResult = function() {
    this.geometry = null;
    this.distance = 0;
    this.position = Vector3.zero;
    this.normal = Vector3.zero;
};

IntersectResult.noHit = new IntersectResult();

球体

圆球(sphere)是内部二个最简单易行的立体几何图形。那里只思考球体的外表(华为平板),中央点为c、半径为r的圆球表面可用等式(equation)表示:

\left \| \mathbf{x} – \mathbf{c} \right \| = r

如前文所述,须求计算光线和球体的近年交点。只要把光芒x =
r(t)代入球体等式,把该等式求解正是交点。为简化方程,设v=o – c,则:

\begin{align*} \left\| \mathbf{v} +t\mathbf{d} \right\| &= r
\\ \left\| \mathbf{v} +t\mathbf{d} \right\|^2 &= r^2 \\
(\mathbf{v}+t\mathbf{d}) \cdot (\mathbf{v}+t\mathbf{d}) – r^2 &= 0
\\ \mathbf{v}^2 + 2t(\mathbf{d} \cdot \mathbf{v}) +t^2
\mathbf{d}^2 – r^2 &= 0 \\ (\mathbf{d}^2)t^2 + (2\mathbf{d} \cdot
\mathbf{v})t + (\mathbf{v}^2 – r^2) &= 0 \end{align*}

因为d为单位向量,所以一回方的周密能够消去。 t的一回方程式的解为

\begin{align*} t&=\frac{-2\mathbf{d} \cdot \mathbf{v}\pm
\sqrt{(2\mathbf{d} \cdot \mathbf{v})^2 – 4(\mathbf{v}^ 2 – r^2)}
}{2} \\ &= -\mathbf{d} \cdot \mathbf{v}\pm \sqrt{(\mathbf{d}
\cdot \mathbf{v})^2 – (\mathbf {v}^2 – r^2)} \end{align*}

若根号内为负数,即相交不发生。其它,由于此地只供给取近年来的交点,由此正负号只需取负号。代码完结如下:

Sphere = function(center, radius) { this.center = center; this.radius = radius; };

Sphere.prototype = {
    copy : function() { return new Sphere(this.center.copy(), this.radius.copy()); },

    initialize : function() {
        this.sqrRadius = this.radius * this.radius;
    },

    intersect : function(ray) {
        var v = ray.origin.subtract(this.center);
        var a0 = v.sqrLength() - this.sqrRadius;
        var DdotV = ray.direction.dot(v);

        if (DdotV <= 0) {
            var discr = DdotV * DdotV - a0;
            if (discr >= 0) {
                var result = new IntersectResult();
                result.geometry = this;
                result.distance = -DdotV - Math.sqrt(discr);
                result.position = ray.getPoint(result.distance);
                result.normal = result.position.subtract(this.center).normalize();
                return result;
            }
        }

        return IntersectResult.noHit;
    }
};

达成代码时,尽快用最少的运算剔除没相交的事态(Math.sqrt是比较慢的函数)。其它,预计算了球体半径r的平方,此为一个优化。

此地用到叁个IntersectResult类,那几个类只用来记录交点的几何物件(geometry)、距离(distance)、地点(position)和法向量(normal)。
IntersectResult.noHit的geometry为null,代表光线未有和其余几何物件相交。

IntersectResult = function() {
    this.geometry = null;
    this.distance = 0;
    this.position = Vector3.zero;
    this.normal = Vector3.zero;
};

IntersectResult.noHit = new IntersectResult();

  由于像素点都以整数坐标,所以每趟求得的 y 都要取整操作,选取加
0.五 取整数有个别的秘籍:(int)(y[i]+0.5)。


光由光子(电磁粒子)组成,光子由各样光源发射。当一组光子撞击三个实体时,也许产生三种情形:被接收,反射或透射。产生那两种意况的光子百分比因材料而异,经常决定了实体在场地中的显现方式。但是,全数材质都有1个共性:入射光子总数总是与反射光子、吸收光子、透射光子的总数相同。

摄影机

摄电影放映机在强光追踪系统里,负责把影象的抽样地方,生成壹束光线。

是因为印象的高低是可变的(多少像素宽x多少像素高),为方便计算,那里设定2个统1的抽样座标(sx,
sy),以左下角为(0,0),右上角为(一 ,一)。

从数学角度来说,摄影机透过投影(projection),把三个维度空间投射到2维空间上。常见的影子有正投影(orthographic
projection)、透视投影(perspective
projection)等等。那里首先落到实处透视投影。 ]]>

摄影机

水墨画机在光线追踪系统里,负责把印象的取样地方,生成1束光线。

是因为影象的大大小小是可变的(多少像素宽x多少像素高),为便利总括,那里设定二个合并的取样座标(sx,
sy),以左下角为(0,0),右上角为(1 ,一)。

从数学角度来说,水墨画机透过投影(projection),把三个维度空间投射到贰维空间上。常见的黑影有正投影(orthographic
projection)、透视投影(perspective
projection)等等。那里首先达成透视投影。 ]]>

  但是,当 |k| > 一 时,由于 x 每一趟递增
一,会造成取的光栅采集样品点过稀,可能不足以近似趋近 理想的数学意义上的直线。


白光由“红”、“蓝”、“绿”二种颜色光子组成。当白光照明灰色物体时,光子吸收进程会过滤掉“玉绿”和“深蓝”光子。因为物体不吸收“青绿”光子,所以它们将被反射,那正是实体显示深紫灰的来头。

透视摄电影放映机

透视水墨画机比较像眼睛和实际雕塑机的原理,能展现远小近大的体察措施。透视投影从视点(view
point/eye position),向有个别方向观看气象,观察的角度范围称为视野(田野 of
view,
FOV)。除了定义观看的前进(forward)是10分样子,还供给定义在形象平面中,何谓上下和左右。为简便起见,权且不思量宽高不等的印象,FOV同时表示水平和垂直方向的视野角度。

图片 7

上海教室展现,从油画机上方展现的几个参数。
forward和right分别是向前和向右的单位向量。

因为视点是原则性的,光线的源点不变。要生成光线,只须用取样座标(sx,
sy)总括其方向d。留意FOV和s的涉及为:

\tan \frac{FOV}{2} = s

把sx从[0,
1]映射到[-1,1],就可以用right向量和s,来计量r向量,代码如下:

PerspectiveCamera = function(eye, front, up, fov) { this.eye = eye; this.front = front; this.refUp = up; this.fov = fov; };

PerspectiveCamera.prototype = {
    initialize : function() {
        this.right = this.front.cross(this.refUp);
        this.up = this.right.cross(this.front);
        this.fovScale = Math.tan(this.fov * 0.5 * Math.PI / 180) * 2;
    },

    generateRay : function(x, y) {
        var r = this.right.multiply((x - 0.5) * this.fovScale);
        var u = this.up.multiply((y - 0.5) * this.fovScale);
        return new Ray3(this.eye, this.front.add(r).add(u).normalize());
    }
};

代码中fov为度数,转为弧度才能动用Math.tan()。别的,fovScale预先乘了二,因为sx映射到[-1,1]每一趟都要倍加2。
sy和sx的做法一样,把七个在形象平面包车型客车向量,加上forward向量,就变成光线方向d。因随后的测算须要,最后把d变成单位向量。

透视摄影机

透视雕塑机相比像眼睛和真实性水墨画机的规律,能突显远小近大的洞察措施。透视投影从视点(view
point/eye position),向某些方向观看气象,观望的角度范围称为视野(田野(field) of
view,
FOV)。除了定义阅览的前行(forward)是这么些样子,还亟需定义在形象平面中,何谓上下和左右。为不难起见,临时不思考宽高不等的形象,FOV同时期表水平和垂直方向的视野角度。

图片 8

上航海用体育场面浮现,从油画机上方显示的多少个参数。
forward和right分别是前进和向右的单位向量。

因为视点是固定的,光线的源点不变。要生成光线,只须用取样座标(sx,
sy)总括其方向d。留意FOV和s的涉嫌为:

\tan \frac{FOV}{2} = s

把sx从[0,
1]映射到[-1,1],就足以用right向量和s,来测算r向量,代码如下:

PerspectiveCamera = function(eye, front, up, fov) { this.eye = eye; this.front = front; this.refUp = up; this.fov = fov; };

PerspectiveCamera.prototype = {
    initialize : function() {
        this.right = this.front.cross(this.refUp);
        this.up = this.right.cross(this.front);
        this.fovScale = Math.tan(this.fov * 0.5 * Math.PI / 180) * 2;
    },

    generateRay : function(x, y) {
        var r = this.right.multiply((x - 0.5) * this.fovScale);
        var u = this.up.multiply((y - 0.5) * this.fovScale);
        return new Ray3(this.eye, this.front.add(r).add(u).normalize());
    }
};

代码中fov为度数,转为弧度才能采用Math.tan()。其余,fovScale预先乘了二,因为sx映射到[-1,1]每一遍都要加倍贰。
sy和sx的做法一样,把七个在形象平面的向量,加上forward向量,就成为光线方向d。因随后的测算需求,最终把d变成单位向量。

  该算法的频率是:浮点数加法级别的。


大家由此能够见到物体,是因为物体反射的有个别光子向我们传播并击中了我们的眸子。大家的肉眼由光感受器组成,能够将光时限信号转换为神经实信号,然后大家的大脑能够利用那几个时域信号来鉴定区别分裂的阴影和色泽。

渲染测试

写了Vector叁、Ray三、Sphere、IntersectResult、Camera三个类之后,终于得以起来渲染一点东西出来!

主干的做法是遍历影象的抽样座标(sx, sy),用Camera把(sx,
sy)转为Ray叁,和情景(例如Sphere)总结近日亲交配点,把该交点的属性转为颜色,写入影像的相对地方里。

把区别的天性渲染出来,是CG编程里不时用的测试和调剂手法。小编也是用此办法,改良了部分荒谬。

渲染测试

写了Vector三、Ray三、Sphere、IntersectResult、Camera七个类之后,终于得以伊始渲染一点东西出来!

中央的做法是遍历影象的抽样座标(sx, sy),用Camera把(sx,
sy)转为Ray叁,和气象(例如Sphere)总结近日亲交配点,把该交点的性子转为颜色,写入印象的对峙地方里。

把分化的本性渲染出来,是CG编程里平常用的测试和调节手法。我也是用此办法,校对了一些漏洞非常多。

 

叁、光与实体的涉及

渲染深度

深度(depth)就是从IntersectResult取得近来相交点的距离,因深度的限定是从零至无限,为了把它展现出来,能够把它的二个区间映射到灰阶。那里用[0,
maxDepth]映射至[255,
0],即深度0的像素为浅青,深度达maxDepth的像素为浅绿。

// renderDepth.htm
function renderDepth(canvas, scene, camera, maxDepth) {
    // 从canvas取得imgdata和pixels,跟之前的代码一样
    // ...

    scene.initialize();
    camera.initialize();

    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 result = scene.intersect(ray);
            if (result.geometry) {
                var depth = 255 - Math.min((result.distance / maxDepth) * 255, 255);
                pixels[i    ] = depth;
                pixels[i + 1] = depth;
                pixels[i + 2] = depth;
                pixels[i + 3] = 255;
            }
            i += 4;
        }
    }

    ctx.putImageData(imgdata, 0, 0);
}

renderDepth( document.getElementById(‘depthCanvas’), new Sphere(new
Vector3(0, 10, -10), 10), new PerspectiveCamera(new Vector3(0, 10, 10),
new Vector3(0, 0, -1), new Vector3(0, 1, 0), 90), 20);

Run

 

这里的观看方向是,正X轴向右,正Y轴向上,正Z轴向后。

修改代码试试看

  • 改变球体的位置
  • 改变球体的半径
  • 改变fov(PerspectiveCamera最后的参数)
  • 改变maxDepth(renderDepth最后的参数)
  • 改变摄影机的方向,例如向左转一点点(记得要是单位向量啊!可以用new Vector(…).normalize())

渲染深度

纵深(depth)正是从IntersectResult取得近日相交点的相距,因深度的界定是从零至Infiniti,为了把它展现出来,能够把它的1个间距映射到灰阶。那里用[0,
maxDepth]映射至[255,
0],即深度0的像素为樱草黄,深度达maxDepth的像素为湖蓝。

// renderDepth.htm
function renderDepth(canvas, scene, camera, maxDepth) {
    // 从canvas取得imgdata和pixels,跟之前的代码一样
    // ...

    scene.initialize();
    camera.initialize();

    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 result = scene.intersect(ray);
            if (result.geometry) {
                var depth = 255 - Math.min((result.distance / maxDepth) * 255, 255);
                pixels[i    ] = depth;
                pixels[i + 1] = depth;
                pixels[i + 2] = depth;
                pixels[i + 3] = 255;
            }
            i += 4;
        }
    }

    ctx.putImageData(imgdata, 0, 0);
}

renderDepth( document.getElementById(‘depthCanvas’), new Sphere(new
Vector3(0, 10, -10), 10), new PerspectiveCamera(new Vector3(0, 10, 10),
new Vector3(0, 0, -1), new Vector3(0, 1, 0), 90), 20);

Run

 

这里的观看方向是,正X轴向右,正Y轴向上,正Z轴向后。

修改代码试试看

  • 改变球体的位置
  • 改变球体的半径
  • 改变fov(PerspectiveCamera最后的参数)
  • 改变maxDepth(renderDepth最后的参数)
  • 改变摄影机的方向,例如向左转一点点(记得要是单位向量啊!可以用new Vector(…).normalize())

贰、中式点心画线法:


未有光泽,大家都看不到周围的实体。

渲染法向量

会友测试也算算了几何物件在交接地方的法向量,那里也可把它视觉化。法向量是1个单位向量,其每种成分的限制是[-1,
1]。把单位向量映射到颜色的常用方法为,把(x, y, z)映射至(r, g,
b),范围从[-1, 1]映射至[0, 255]。

// renderNormal.htm
function renderNormal(canvas, scene, camera) {
    // ...
            if (result.geometry) {
                pixels[i    ] = (result.normal.x + 1) * 128;
                pixels[i + 1] = (result.normal.y + 1) * 128;
                pixels[i + 2] = (result.normal.z + 1) * 128;
                pixels[i + 3] = 255;
            }
    // ...
}

renderNormal( document.getElementById(‘normalCanvas’), new Sphere(new
Vector3(0, 10, -10), 10), new PerspectiveCamera(new Vector3(0, 10, 10),
new Vector3(0, 0, -1), new Vector3(0, 1, 0), 90), 20);

Run

 

球体上方的法向量是接近(0, 1, 0),所以是浅绿色(0.5, 1, 0.5)。

修改代码试试看

  • 从球体的正上方往下看

渲染法向量

结交测试也算算了几何物件在交接地方的法向量,那里也可把它视觉化。法向量是二个单位向量,其每种元素的限定是[-1,
1]。把单位向量映射到颜色的常用方法为,把(x, y, z)映射至(r, g,
b),范围从[-1, 1]映射至[0, 255]。

// renderNormal.htm
function renderNormal(canvas, scene, camera) {
    // ...
            if (result.geometry) {
                pixels[i    ] = (result.normal.x + 1) * 128;
                pixels[i + 1] = (result.normal.y + 1) * 128;
                pixels[i + 2] = (result.normal.z + 1) * 128;
                pixels[i + 3] = 255;
            }
    // ...
}

renderNormal( document.getElementById(‘normalCanvas’), new Sphere(new
Vector3(0, 10, -10), 10), new PerspectiveCamera(new Vector3(0, 10, 10),
new Vector3(0, 0, -1), new Vector3(0, 1, 0), 90), 20);

Run

 

球体上方的法向量是接近(0, 1, 0),所以是浅绿色(0.5, 1, 0.5)。

修改代码试试看

  • 从球体的正上方往下看

  此算法依旧根据增量思想。


相近环境中尚无实体,大家看不到光。

材质

渲染深度和法向量只为测试和调剂,要显得物件的”真实”颜色,供给定义该交点向某方向(如往视点的倾向)发出的光的颜料,称之为多少个图形的质感(material
)。

材料的接口为function sample(ray, posiiton, normal)
,传回颜色Color的靶子。那是个极简陋的接口,权且做1些效应出来,有时机再详谈。

材质

渲染深度和法向量只为测试和调节和测试,要展现物件的”真实”颜色,要求定义该交点向某方向(如往视点的趋向)发出的光的水彩,称之为几个图形的质量(material
)。

材料的接口为function sample(ray, posiiton, normal)
,传回颜色Color的目的。那是个极简陋的接口,一时半刻做1些作用出来,有机遇再详谈。

  对于直线的一般式:Ax+By+C=0,当|k|<1时,设想每便 x 递增
壹,y[i+1] 都只大概分外 y[i] 或者
y[i]+一,它取决于对于核心固有误差项的判断。

二、光线追踪(RayTracing)算法描述

颜色

颜色在CG里最简就是用红、绿、蓝四个通道(color
channel)。为达成简单的Phong材料,还投入了对颜色的简约操作。

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); },
    multiply : function(s) { return new Color(this.r * s, this.g * s, this.b * s); },
    modulate : 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);

那Color类很像Vector三类,值得留意的是,颜色有调制(modulate)操作,其意思为三个颜色中种种颜色通道相乘。

颜色

颜色在CG里最简易是用红、绿、蓝多个通道(color
channel)。为贯彻简单的Phong材料,还进入了对颜色的简易操作。

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); },
    multiply : function(s) { return new Color(this.r * s, this.g * s, this.b * s); },
    modulate : 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);

这Color类很像Vector三类,值得注意的是,颜色有调制(modulate)操作,其意义为七个颜色中各种颜色通道相乘。

  图片 9