您的位置:首页 > 编程语言 > Java开发

java 光线追踪

2015-11-09 17:57 309 查看
代码改变世界
Posts - 30, Articles - 0, Comments - 1298
Cnblogs
Dashboard
Login

Home
Contact
Gallery
RSS

Milo的游戏开发

用JavaScript玩转计算机图形学(一)光线追踪入门

2010-03-29 00:05 by Milo Yip, 40942 阅读,
105 评论,
收藏,
编辑



系列简介

记得小时候读过一本关于计算机图形学(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)。换句话说,渲染几何图形的一个像素时,光照计算只能取得该像素的资讯,而不能访问其他几何图形资讯。理论上,阴影(shadow)、反射(reflection)、折射(refraction)等为全局光照(global
illumination)效果,实际上,栅格化渲染系统可以使用预处理(如阴影贴图(shadow mapping)、环境贴图(environment mapping))去模拟这些效果。

全局光照计算量大,一般也没有特殊硬件加速(通常只使用CPU而非GPU),所以只适合离线渲染(offline rendering),例如3D Studio Max、Maya等工具。其中一个支持全局光照的方法,称为光线追踪(ray tracing)。光线追踪能简单直接地支持阴影、反射、折射,实现起来亦非常容易。本文的例子里,只用了数十行JavaScript代码(除canvas外不需要其他特殊插件和库),就能实现一个支持反射的光线追踪渲染器。光线追踪可以用来学习很多计算机图形学的课题,也许比学习Direct3D/OpenGL更容易。现在,先介绍点理论吧。

光线追踪

光栅化渲染,简单地说,就是把大量三角形画到屏幕上。当中会采用深度缓冲(depth buffer, z-buffer),来解决多个三角形重叠时的前后问题。三角形数目影响效能,但三角形在屏幕上的总面积才是主要瓶颈。

光线追踪,简单地说,就是从摄影机的位置,通过影像平面上的像素位置(比较正确的说法是取样(sampling)位置),发射一束光线到场景,求光线和几何图形间最近的交点,再求该交点的著色。如果该交点的材质是反射性的,可以在该交点向反射方向继续追踪。光线追踪除了容易支持一些全局光照效果外,亦不局限于三角形作为几何图形的单位。任何几何图形,能与一束光线计算交点(intersection point),就能支持。



上图(來源)显示了光线追踪的基本方式。要计算一点是否在阴影之内,也只须发射一束光线到光源,检测中间有没有障碍物而已。不过光源和阴影留待下回分解。

初试画板

光线追踪的输出只是一个影像(image),所谓影像,就是二维颜色数组。

要在浏览器内,用JavaScript生成一个影像,目前可以使用HTML 5的<canvas>。但现时Internet Explorer(直至版本8)还不支持<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定義如下:

?

修改代码试试看

把第三个pixels[i++] = 0 改为255 (即蓝色全开)
把第四个pixels[i++] = 255 改为128 (alpha=128)
可以只修改两个for循环里面的代码,画一个国际象棋棋盘么?
这实验说明,从canvas取得的影像资料canvas.getImageData(...).data是个一维数组,该数组每四个元素代表一个象素(按红, 绿, 蓝, alpha排列),这些象素在影像中从上至下、左至右排列。

解决实验平台的技术问题后,可开始从基础类别开始实现。

基础类

笔者使用基于物件(object-based)的方式编写JavaScript。

三维向量

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

?
这些类方法(如normalize、negate、add等),如果传回Vector3类对象,都会传回一个新建构的Vector3。这些三维向量的功能很简单,不在此详述。注意multiply和divide是与纯量(scalar)相乘和相除。

Vector3.zero用作常量,避免每次重新构建。值得一提,这些常量必需在prototype设定之后才能定义。

光线

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



当中,o即发谢起点(origin),d为方向。在本文的例子里,都假设d为单位向量(unit vector),因此t为距离。实现如下:

?

球体

球体(sphere)是其中一个最简单的立体几何图形。这里只考虑球体的表面(surface),中心点为c、半径为r的球体表面可用等式(equation)表示:



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



因为d为单位向量,所以二次方的系数可以消去。 t的二次方程式的解为



若根号内为负数,即相交不发生。另外,由于这里只需要取最近的交点,因此正负号只需取负号。代码实现如下:

?
实现代码时,尽快用最少的运算剔除没相交的情况(Math.sqrt是比较慢的函数)。另外,预计算了球体半径r的平方,此为一个优化。

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

?

摄影机

摄影机在光线追踪系统里,负责把影像的取样位置,生成一束光线。

由于影像的大小是可变的(多少像素宽x多少像素高),为方便计算,这里设定一个统一的取样座标(sx, sy),以左下角为(0,0),右上角为(1 ,1)。

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

透视摄影机

透视摄影机比较像肉眼和真实摄影机的原理,能表现远小近大的观察方式。透视投影从视点(view point/eye position),向某个方向观察场景,观察的角度范围称为视野(field of view, FOV)。除了定义观察的向前(forward)是那个方向,还需要定义在影像平面中,何谓上下和左右。为简单起见,暂时不考虑宽高不同的影像,FOV同时代表水平和垂直方向的视野角度。



上图显示,从摄影机上方显示的几个参数。 forward和right分别是向前和向右的单位向量。

因为视点是固定的,光线的起点不变。要生成光线,只须用取样座标(sx, sy)计算其方向d。留意FOV和s的关系为:



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

?
代码中fov为度数,转为弧度才能使用Math.tan()。另外,fovScale预先乘了2,因为sx映射到[-1,1]每次都要乘以2。 sy和sx的做法一样,把两个在影像平面的向量,加上forward向量,就成为光线方向d。因之后的计算需要,最后把d变成单位向量。

渲染测试

写了Vector3、Ray3、Sphere、IntersectResult、Camera五个类之后,终于可以开始渲染一点东西出来!

基本的做法是遍历影像的取样座标(sx, sy),用Camera把(sx, sy)转为Ray3,和场景(例如Sphere)计算最近交点,把该交点的属性转为颜色,写入影像的相对位置里。

把不同的属性渲染出来,是CG编程里经常用的测试和调试手法。笔者也是用此方法,修正了一些错误。

渲染深度

深度(depth)就是从IntersectResult取得最近相交点的距离,因深度的范围是从零至无限,为了把它显示出来,可以把它的一个区间映射到灰阶。这里用[0, maxDepth]映射至[255, 0],即深度0的像素为白色,深度达maxDepth的像素为黑色。

?
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]。把单位向量映射到颜色的常用方法为,把(x, y, z)映射至(r, g, b),范围从[-1, 1]映射至[0, 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的对象。这是个极简陋的接口,临时做一些效果出来,有机会再详谈。

颜色

颜色在CG里最简单是用红、绿、蓝三个通道(color channel)。为实现简单的Phong材质,还加入了对颜色的简单操作。

?
这Color类很像Vector3类,值得留意的是,颜色有调制(modulate)操作,其意义为两个颜色中每个颜色通道相乘。

格子材质

CG世界里,国际象棋棋盘是最常见的测试用纹理(texture)。这里不考虑纹理贴图(texture mapping)的问题,只凭(x, z)坐标计算某位置发出黑色或白色的光(黑色的光不叫光吧,哈哈)。

?
代码中scale的意义为1坐标单位有多少个格子,例如scale=0.1即一个格子的大小为10x10。

Phong材质

这里实现简单的Phong材质,因为未有光源系统,只用全域变量设置一个临时的光源方向,并只计算漫射(diffuse)和镜射(specular)。

?
Phong的内容不在此述。

渲染材质

修改之前的渲染代码,当碰到相交时,就向几何对象取得material属性,并调用sample方法函数取得颜色。

?
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);
sphere1.material = new PhongMaterial(Color.red, Color.white, 16);
sphere2.material = new PhongMaterial(Color.blue, Color.white, 16);
rayTrace(
document.getElementById('rayTraceCanvas'),
new Union([plane, sphere1, sphere2]),
new PerspectiveCamera(new Vector3(0, 5, 15), new Vector3(0, 0, -1), new Vector3(0, 1, 0), 90));

Run

 

修改代码试试看

改变fov,有了格子地板效果应该很明显
改变CheckerMaterial的scale
把原来红色的球改为绿色
把原来红色的球改为黄色
改变shininess(PhongMaterial最后一个参数)

多个几何物件

只渲染一个几何物件太乏味,这节再加入一个无限平面,和介绍如何组合多个几何物件。

平面

一个(无限)平面(Plane)在数学上可用等式定义:



n为平面的法向量,d为空间原点至平面的最短距离。光线和平面的相交计算很简单,这里不详述了。

?

并集

把多个几何物件结合起来,可以使用集(set)的概念。这里最容易实现的操作,就是并集(union),即光线要找到一组几个图形的最近交点。无需改其他代码,只加入一个Union类就可以:

?
可以看到,这里利用Javascript的多型(polymorphism)的特性,完全不用修改原来的代码,就可以扩展功能。

如前所述,这里只考虑几何几何图形的表面。如果考虑几何图形是实心的,就可以用构造实体几何(constructive solid geometry, CSG)方法,提供并集、交集、补集等操作。容后再谈。

反射

以上实现的,也只是局部照明。只要再加入一点点代码,就可以实现反射。

下图说明反射向量的计算方法:



把d投射到n上(因n是单位向量,只需要点乘即可),就可以计算d在n上的长度,把d减去这长度两倍的法向量,就是反射向量r。数学上可写成:



一般材质并非完全反射(镜子除外),因此这里为材质加上一个反射度(reflectiveness)的属性。反射的功能很简单,只要在碰到反射度非零的材质,就继续向反射方向追踪,并把结果按反射度来混合。例如一个材质的反射度为25%,则它传回的颜色是75%本身颜色,加上25%反射传回来的颜色。

另外,不断反射会做成大量的运算,甚至乎永远不能停止(考虑摄影机在两个镜子中间)。因此要限制反射的次数。含反射功能的光线追踪代码如下:

?
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('rayTraceReflectionCanvas'),
new Union([plane, sphere1, sphere2]),
new PerspectiveCamera(new Vector3(0, 5, 15), new Vector3(0, 0, -1), new Vector3(0, 1, 0), 90),
3);

Run

 

修改代码试试看

改变一个球的reflectiveness,试试0、1及之间的数值
改变maxReflect(rayTraceReflection最后一个参数)
加入更多的球体(可用for循环啊……不过小心渲染时间太长)

结语

能体会到计算机图形学的有趣之处么?百多行简单的JavaScript代码,就绘画出像真的影像,那种满足感实非笔墨所能形容。

本文实现了一个简单的光线追踪渲染器,支持球体、平面、Phong材质、格子材质、多重反射等功能。读者可以下载这组代码,加入不同的扩展,也可以尝试翻译做熟悉的编程语言。很多光线追踪用到的计算机图形技术,也可以应用到实时图形编程里,例如光源和材质的计算,基本上可以简易翻译做实时图形的著色器(shader)编程。

游戏里采用光栅化渲染技术已有二十年以上,这几年的硬件发展,使其他渲染方法也能用于实时应用。光线追踪和其他类似的方法,有个当今重要优点,就是能高度平行化。采样之间并没有依赖性,例如256x256=65536个采样,理论上,可使用65536个机器/核心独立执行追踪,那么完成时间只是最慢的一个取样所需的时间。

笔者希望继续撰写这系列,例如包括以下内容:

其他几何图形(长方体、柱体、三角形、曲面、高度场、等值面、……)
光源(方向光源、点光源、聚光灯、阴影、ambient occlusion)
材质(Phong-Blinn、Oren-Nayar、Torrance-Sparrow、折射、 Fresnel、BRDF、BSDF……)
纹理(纹理座标、采样、Perlin noise)
摄影机模型(正投射、全景、景深)
成像流程(渐进渲染、反锯齿、后期处理)
优化方法(场景剖分、低阶优化)
其他全局光照渲染方法
祈望得到大家的意见反馈。

参考

Matt Pharr, Greg Humphreys, Physically Based Rendering, Morgan Kaufmann, 2004

Wikipedia,
Ray Tracing
Slime, The JavaScript Raytracer
SIGGRAPH HyperGraph Education Project,
Ray Tracing

更新

2010年3月31日,网友HouSisong把本文代码以C++实现,并完全保留了原设计,代码可於他的博文下载。

好文要顶
关注我 收藏该文联系我








Milo Yip
关注 - 31
粉丝 - 1652

荣誉:推荐博客
+加关注

87
1

(请您对文章做出评价)

«
上一篇:12年前的作品──《美绿中国象棋》制作过程及算法简介
»
下一篇:用JavaScript玩转计算机图形学(二)基本光源

分类:
计算机图形学

< Prev123

Add your comment

#101楼

君德
  2014-12-03 13:37

读了楼主的文章,茅塞顿开,拜服
支持(0)反对(0)

#102楼

codeworm
  2015-01-07 11:59

碉堡了
支持(0)反对(0)

#103楼

sqxieshuai
  2015-01-27 20:48


?
这里的this.radius是一个number。为什么需要copy呢?
支持(0)反对(0)

#104楼

langlangmxl
  2015-05-17 16:31

楼主 你好 我现在看你的这篇文章很是吃力几乎看不懂,希望楼主推荐一本相关的书籍给我从基础开始学习

谢谢
支持(0)反对(0)

#105楼32597952015/9/4
13:20:04
灰14
  2015-09-04 13:20

这篇博文实在太棒了,简直就是我看过的最好的博文,让我这种完全没有计算机图形学基础的菜鸟也看得津津有味!希望博主有精力的话能继续这个系列,博主太nice了!
支持(0)反对(0)

< Prev123

刷新评论刷新页面返回顶部

注册用户登录后才能发表评论,请
登录 或 注册,访问网站首页。

【推荐】50万行VC++源码: 大型组态工控、电力仿真CAD与GIS源码库
【推荐】融云即时通讯云-专注为 App 开发者提供IM云服务
【福利】极光推送-富媒体+大数据驱动精准推送,完全免费开放啦
【专享】阿里云9折优惠码:bky758

最新IT新闻:

· 传阿里巴巴有意投资《南华早报》

· 计算需求向云计算转移 科技寡头垄断优势

· FB与谷歌抢滩无人机:欲通过无人机传输网络

· 十家成长最快的初创公司所具备的十个共同点

· 设计师必须真正理解玩家的反馈

»
更多新闻...

最新知识库文章:
·
被误解的MVC和被神化的MVVM

· 再谈设计和编码

· 什么时候应该避免写代码注释?

· 持续集成是什么?

· 人,技术与流程

» 更多知识库文章...

About

Milo Yip是香港同胞,现任职于深圳腾讯互动娱乐事业群研发部引擎技术中心。

在高一高二时兼职开发游戏《王子传奇》后,便潜心向学(伪),得到认知科学学士和系统工程及工程管理学哲学硕士后, 于大学里做游戏科技的相关项目,直至2008年才来到上海,再次投身游戏业界,之前的作品是《美食从天而降》Xbox360/PS3/Wii/PC、《爱丽丝:疯狂回归》Xbox360/PS3/PC。

因为写了一个书评而认识到很多朋友,决定在内地设一个blog,和大家分享交流。

Twitter

新浪微博

昵称:Milo Yip

园龄:5年9个月

荣誉:推荐博客

粉丝:1652

关注:31
+加关注

最新评论

Re:爱丽丝的发丝──《爱丽丝惊魂记:疯狂再临》制作点滴

原来玩的时候以为头发是直接PhysX模拟的呢...原来如此。

用过您写的rapidjson替换掉原有的jsoncpp,效率很不错,没想到以前还玩过的您写的游戏,膜拜一下~ -- Macworld
Re:C++/C#/F#/Java/JS/Lua/Python/Ruby渲染比试

楼主讲的很详细,必须赞一个!但是对于我们初学者很费劲,要是能图文并茂深入讲解就好了,我给你推荐一个不错的博文。Python基本语法,python入门到精通[二]最近园子新开的python系列,一步步从...... -- 成王之路

Re:爱丽丝的发丝──《爱丽丝惊魂记:疯狂再临》制作点滴

@tony.xiao 感觉你好亏呀 。。。 气了这么多年 莫生气 生气伤身 -- dolormine

Re:用JavaScript玩转计算机图形学(一)光线追踪入门

这篇博文实在太棒了,简直就是我看过的最好的博文,让我这种完全没有计算机图形学基础的菜鸟也看得津津有味!希望博主有精力的话能继续这个系列,博主太nice了! -- 灰14

Re:RapidJSON 代码剖析(四):优化 Grisu

@Milo,谢谢你的分享,非常的有参考价值我最近在实现快速YAML配置解析与键值访问,解析速度比yaml-cpp快70倍,比jsoncpp快8倍,而多层键值访问速度也比jsoncpp快3倍。我比较奇怪...... -- 自由的粒子

随笔档案

2015年6月(2)
2015年5月(2)
2014年2月(2)
2013年4月(1)
2011年6月(1)
2010年9月(2)
2010年8月(1)
2010年7月(1)
2010年6月(3)
2010年5月(2)
2010年4月(5)
2010年3月(4)
2010年2月(4)

日历

<2010年3月>
28123456
78910111213
14151617181920
21222324252627
28293031123
45678910

随笔分类

计算机图形学(5)

人工智能(1)

数据结构和算法(6)

物理模拟(1)

游戏编程(5)

游戏引擎(6)

杂谈(8)

推荐排行榜

1. C++强大背后(150)
2. 用JavaScript玩转游戏物理(一)运动学模拟与粒子系统(127)
3. 用JavaScript玩转计算机图形学(二)基本光源(104)
4. 回应CSDN肖舸《做程序,要“专注”和“客观”》,实验比较各离散采样算法(101)
5. 爱丽丝的发丝──《爱丽丝惊魂记:疯狂再临》制作点滴(98)

阅读排行榜

1. 爱丽丝的发丝──《爱丽丝惊魂记:疯狂再临》制作点滴(73982)
2. C++强大背后(61349)
3. C++/C#/F#/Java/JS/Lua/Python/Ruby渲染比试(46360)
4. 用JavaScript玩转计算机图形学(一)光线追踪入门(40952)
5. 史上最强女游戏程序员(39884)

www.spiga.com.mx

Copyright ©2015 Milo Yip

博客园
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: