## 用JavaScript玩转计算机图形学(二)基本光源

2010-04-02 20:44 by Milo Yip, ... 阅读, ... 评论, 收藏, 编辑

## 方向光源

### 实现DirectionalLight类

LightSample = function(L, EL) { this.L = L; this.EL = EL; };
LightSample.zero = new LightSample(Vector3.zero, Color.black);


DirectionalLight = function(irradiance, direction) { this.irradiance = irradiance; this.direction = direction; this.shadow = true; };

DirectionalLight.prototype = {
initialize: function() { this.L = this.direction.normalize().negate(); },

sample: function(scene, position) {
// 阴影测试
var shadowRay = new Ray3(position, this.L);
return LightSample.zero;
}

}
};


### 渲染幅照度

sample()函数可以传回相对光向量的幅照度，但物体表面并不一定垂直于光向量。光源越接近平面，每面积接受的能量就越少。可以想像太阳在中午是最亮的，日出日落时是最暗的。如下图所示，平面法向量方向的面积，是光向量方向的面积的1/\cos\theta_i"倍，而幅照度则为其倒数，即\cos\theta_i"倍。

\begin{align*} E&=E_L\max(\cos\theta_i, 0) \\ &=E_L\max(\mathbf{n }\cdot\mathbf{l}, 0) \end{align*}

\begin{align*} E&=\sum_{k=1}^{n}E_{L_k}\max(\cos\theta_{ i_k} 0) \\ &=\sum_{k=1}^{n}E_{L_k}\max(\mathbf{n}\cdot\mathbf{l}_k, 0) \end{align*}

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

scene.initialize();
for (var k in lights)
lights[k].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 color = Color.black;
for (var k in lights) {
var lightSample = lights[k].sample(scene, result.position);

if (lightSample != lightSample.zero) {
var NdotL = result.normal.dot(lightSample.L);

// 夹角小约90度，即光源在平面的前面
if (NdotL >= 0)
}
}
pixels[i] = color.r * 255;
pixels[i + 1] = color.g * 255;
pixels[i + 2] = color.b * 255;
pixels[i + 3] = 255;
}
i += 4;
}
}

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



#### 修改代码试试看

• 改變光源的顏色 (也試試超過1的值)
改變光源的方向 (在DirectionalLight.initialize()裡自動做了normalize，這輸入不需位單位向量)
改變光源的幅照度 (也試試超過1的值)
• 改變光源的方向 (在DirectionalLight.initialize()裡自動做了normalize，這輸入不需位單位向量)

## 点光源

E_L=\frac{I_L}{r^2}

1/r^2通常称为衰减(attenuation)系数。有时候会为各种需求，写一些非物理正确的衰减系数。

### 实现PointLight类

PointLight = function(intensity, position) { this.intensity = intensity; this.position = position; this.shadow = true; };

PointLight.prototype = {
initialize: function() { },
sample: function(scene, position) {
// 计算L，但保留r和r^2，供之后使用
var delta = this.position.subtract(position);
var rr = delta.sqrLength();
var r = Math.sqrt(rr);
var L = delta.divide(r);

// 阴影测试
var shadowRay = new Ray3(position, L);
// 在r以内的相交点才会遮蔽光源
return LightSample.zero;
}

// 平方反比衰减
var attenuation = 1 / rr;

// 计算幅照度
return new LightSample(L, this.intensity.multiply(attenuation));
}
};


• 改变幅射强度
• 移动光源
• 加入多一个点光源

## 聚光灯

spot(\alpha)= \begin{cases} 1, & \text{where} \cos\alpha \geq \cos \frac{\theta}{2} \\ \left ( \dfrac{\cos\alpha - \cos{\frac{\phi}{2}}}{\cos\frac{\theta}{2}-\cos\frac{\phi}{2} } \right )^p, & \text{where} \cos\frac{\phi}{2} < \cos\alpha < \cos\frac{\theta}{2} \\ 0, & \text{where} \cos\alpha \leq \cos\frac{\phi}{2} \end{cases}

### 实现SpotLight类

SpotLight类只是多了那几个参数，以计算聚光灯系数，最后结合到幅照度。很多参数可在initialize()里预计算，减少在sample()里重复运算。

SpotLight = function(intensity, position, direction, theta, phi, falloff) {
this.intensity = intensity;
this.position = position;
this.direction = direction;
this.theta = theta;
this.phi = phi;
this.falloff = falloff;
};

SpotLight.prototype = {
initialize: function() {
this.S = this.direction.normalize().negate();
this.cosTheta = Math.cos(this.theta * Math.PI / 180 / 2);
this.cosPhi = Math.cos(this.phi * Math.PI / 180 / 2);
this.baseMultiplier = 1 / (this.cosTheta - this.cosPhi);
},

sample: function(scene, position) {
// 计算L，但保留r和r^2，供之后使用
var delta = this.position.subtract(position);
var rr = delta.sqrLength();
var r = Math.sqrt(rr);
var L = delta.divide(r);

// 计算聚光灯因子
var spot;
var SdotL = this.S.dot(L);
if (SdotL >= this.cosTheta)
spot = 1;
else if (SdotL <= this.cosPhi)
spot = 0;
else
spot = Math.pow((SdotL - this.cosPhi) * this.baseMultiplier, this.falloff);

// 阴影测试
var shadowRay = new Ray3(position, L);
// 在r以内的相交点才会遮蔽光源
return LightSample.zero;
}

// 平方反比衰减
var attenuation = 1 / rr;

// 计算幅照度
return new LightSample(L, this.intensity.multiply(attenuation * spot));
}
};


• 改变各个参数

## 例子

### 三原色

#### 修改代码试试看

• 如果，幅射强度是负值的话，会怎么样？(虽然未证实反光子(antiphoton)的存在，但读者能想到图形学上的功能么？)

### 很多光源

#### 修改代码试试看

• 把光源放在不同位置(例如接近地面)
• 把每个光源的颜色加入差异

## 参考

• Tomas Möller, Eric Haines, Naty Hoffman, Real-time Rendering 3rd Edition, AK Peters 2008
• Matt Pharr, Greg Humphreys, Physically Based Rendering, Morgan Kaufmann, 2004