高氏着色
概念
以如果你已经成功的理解了平面着色,那么你会发现高氏着色并不复杂。这次我们不仅针对每个面赋予一个颜色,而是根据三角形的顶点使用 3 个法线。然后我们定义颜色的 3 个级别,使用插值在之前的教程中使用相同的算法对每个顶点之间的像素赋予颜色。使用这种插值,我们将得到三角形连续的光影效果。
图片摘取自: 教程 5.地形 - 光与顶点法线向量
你可以在这张图中看出平面着色和高氏着色的区别。平面着色采用了居中的独有法线,高氏着色则使用了 3 个顶点法线。你还可以看看 3D 网格(棱锥),法线是每顶点每面。我的意思是相同的顶点将具有基于我们当前绘制面不同的法线。
让我们回到绘制三角面逻辑中来。有一个很好的方式来说明我们要做的阴影:
摘自: 教程-创建法线贴图 (作者:Ben Cloward)
在该图中,假设上方顶点有一个>90 度夹角的光的方向,它的颜色应该是黑色的(光的最小级别 = 0)。想象一下现在的其他两个顶点法线与光的方向角度为 0 度,这意味着他们应受到光的最大级别(1)。
为了填充我们的三角形,我们还需要用到插值来使每个顶点之间的颜色有一个很好的过渡。
实现代码
因为代码非常简单,稍作阅读就能够理解我实现的颜色插值了。
【译者注:C#代码】
// 在两点之间从左往右画条线
// papb -> pcpd
// pa, pb, pc, pd 需要先进行排序
void ProcessScanLine(ScanLineData data, Vertex va, Vertex vb, Vertex vc, Vertex vd, Color4 color)
{
Vector3 pa = va.Coordinates;
Vector3 pb = vb.Coordinates;
Vector3 pc = vc.Coordinates;
Vector3 pd = vd.Coordinates;
// 由当前的 y 值,我们可以计算出梯度
// 以此再计算出 起始 X(sx) 和 结束 X(ex)
// 如果 pa.Y == pb.Y 或者 pc.Y== pd.y 的话,梯度强制为 1
var gradient1 = pa.Y != pb.Y ? (data.currentY - pa.Y) / (pb.Y - pa.Y) : 1;
var gradient2 = pc.Y != pd.Y ? (data.currentY - pc.Y) / (pd.Y - pc.Y) : 1;
int sx = (int)Interpolate(pa.X, pb.X, gradient1);
int ex = (int)Interpolate(pc.X, pd.X, gradient2);
// 开始 Z 值和结束 Z 值
float z1 = Interpolate(pa.Z, pb.Z, gradient1);
float z2 = Interpolate(pc.Z, pd.Z, gradient2);
var snl = Interpolate(data.ndotla, data.ndotlb, gradient1);
var enl = Interpolate(data.ndotlc, data.ndotld, gradient2);
// 从左(sx) 向右(ex) 绘制一条线
for (var x = sx; x < ex; x++)
{
float gradient = (x - sx) / (float)(ex - sx);
var z = Interpolate(z1, z2, gradient);
var ndotl = Interpolate(snl, enl, gradient);
// 使用光的向量和法线向量之间的角度余弦来改变颜色值
DrawPoint(new Vector3(x, data.currentY, z), color * ndotl);
}
}
public void DrawTriangle(Vertex v1, Vertex v2, Vertex v3, Color4 color)
{
// 进行排序,p1 总在最上面,p2 总在最中间,p3 总在最下面
if (v1.Coordinates.Y > v2.Coordinates.Y)
{
var temp = v2;
v2 = v1;
v1 = temp;
}
if (v2.Coordinates.Y > v3.Coordinates.Y)
{
var temp = v2;
v2 = v3;
v3 = temp;
}
if (v1.Coordinates.Y > v2.Coordinates.Y)
{
var temp = v2;
v2 = v1;
v1 = temp;
}
Vector3 p1 = v1.Coordinates;
Vector3 p2 = v2.Coordinates;
Vector3 p3 = v3.Coordinates;
// 光照位置
Vector3 lightPos = new Vector3(0, 10, 10);
// 计算光向量和法线向量之间夹角的余弦
// 它会返回介于 0 和 1 之间的值,该值将被用作颜色的亮度
float nl1 = ComputeNDotL(v1.WorldCoordinates, v1.Normal, lightPos);
float nl2 = ComputeNDotL(v2.WorldCoordinates, v2.Normal, lightPos);
float nl3 = ComputeNDotL(v3.WorldCoordinates, v3.Normal, lightPos);
var data = new ScanLineData { };
// 计算线条的方向
float dP1P2, dP1P3;
// http://en.wikipedia.org/wiki/Slope
// 计算斜率
if (p2.Y - p1.Y > 0)
dP1P2 = (p2.X - p1.X) / (p2.Y - p1.Y);
else
dP1P2 = 0;
if (p3.Y - p1.Y > 0)
dP1P3 = (p3.X - p1.X) / (p3.Y - p1.Y);
else
dP1P3 = 0;
if (dP1P2 > dP1P3)
{
for (var y = (int)p1.Y; y <= (int)p3.Y; y++)
{
data.currentY = y;
if (y < p2.Y)
{
data.ndotla = nl1;
data.ndotlb = nl3;
data.ndotlc = nl1;
data.ndotld = nl2;
ProcessScanLine(data, v1, v3, v1, v2, color);
}
else
{
data.ndotla = nl1;
data.ndotlb = nl3;
data.ndotlc = nl2;
data.ndotld = nl3;
ProcessScanLine(data, v1, v3, v2, v3, color);
}
}
}
else
{
for (var y = (int)p1.Y; y <= (int)p3.Y; y++)
{
data.currentY = y;
if (y < p2.Y)
{
data.ndotla = nl1;
data.ndotlb = nl2;
data.ndotlc = nl1;
data.ndotld = nl3;
ProcessScanLine(data, v1, v2, v1, v3, color);
}
else
{
data.ndotla = nl2;
data.ndotlb = nl3;
data.ndotlc = nl1;
data.ndotld = nl3;
ProcessScanLine(data, v2, v3, v1, v3, color);
}
}
}
}
【译者注:TypeScript 代码】
// 在两点之间从左往右画条线
// papb -> pcpd
// pa, pb, pc, pd 需要先进行排序
public processScanLine(data: ScanLineData, va: Vertex, vb: Vertex,
vc: Vertex, vd: Vertex, color: BABYLON.Color4): void {
var pa = va.Coordinates;
var pb = vb.Coordinates;
var pc = vc.Coordinates;
var pd = vd.Coordinates;
// 由当前的 y 值,我们可以计算出梯度
// 以此再计算出 起始 X(sx) 和 结束 X(ex)
// 如果 pa.Y == pb.Y 或者 pc.Y== pd.y 的话,梯度强制为 1
var gradient1 = pa.y != pb.y ? (data.currentY - pa.y) / (pb.y - pa.y) : 1;
var gradient2 = pc.y != pd.y ? (data.currentY - pc.y) / (pd.y - pc.y) : 1;
var sx = this.interpolate(pa.x, pb.x, gradient1) >> 0;
var ex = this.interpolate(pc.x, pd.x, gradient2) >> 0;
// 开始 Z 值和结束 Z 值
var z1: number = this.interpolate(pa.z, pb.z, gradient1);
var z2: number = this.interpolate(pc.z, pd.z, gradient2);
var snl = this.interpolate(data.ndotla, data.ndotlb, gradient1);
var enl = this.interpolate(data.ndotlc, data.ndotld, gradient2);
// 从左(sx) 向右(ex) 绘制一条线
for (var x = sx; x < ex; x++) {
var gradient: number = (x - sx) / (ex - sx);
var z = this.interpolate(z1, z2, gradient);
var ndotl = this.interpolate(snl, enl, gradient);
// 使用光的向量和法线向量之间的角度余弦来改变颜色值
this.drawPoint(new BABYLON.Vector3(x, data.currentY, z),
new BABYLON.Color4(color.r * ndotl, color.g * ndotl, color.b * ndotl, 1));
}
}
public drawTriangle(v1: Vertex, v2: Vertex, v3: Vertex, color: BABYLON.Color4): void {
// 进行排序,p1 总在最上面,p2 总在最中间,p3 总在最下面
if (v1.Coordinates.y > v2.Coordinates.y) {
var temp = v2;
v2 = v1;
v1 = temp;
}
if (v2.Coordinates.y > v3.Coordinates.y) {
var temp = v2;
v2 = v3;
v3 = temp;
}
if (v1.Coordinates.y > v2.Coordinates.y) {
var temp = v2;
v2 = v1;
v1 = temp;
}
var p1 = v1.Coordinates;
var p2 = v2.Coordinates;
var p3 = v3.Coordinates;
// 光照位置
var lightPos = new BABYLON.Vector3(0, 10, 10);
// 计算光向量和法线向量之间夹角的余弦
// 它会返回介于 0 和 1 之间的值,该值将被用作颜色的亮度
//var ndotl = this.computeNDotL(centerPoint, vnFace, lightPos);
var nl1 = this.computeNDotL(v1.WorldCoordinates, v1.Normal, lightPos);
var nl2 = this.computeNDotL(v2.WorldCoordinates, v2.Normal, lightPos);
var nl3 = this.computeNDotL(v3.WorldCoordinates, v3.Normal, lightPos);
var data: ScanLineData = { };
// 计算线条的方向
var dP1P2: number; var dP1P3: number;
// http://en.wikipedia.org/wiki/Slope
// 计算斜率
if (p2.y - p1.y > 0)
dP1P2 = (p2.x - p1.x) / (p2.y - p1.y);
else
dP1P2 = 0;
if (p3.y - p1.y > 0)
dP1P3 = (p3.x - p1.x) / (p3.y - p1.y);
else
dP1P3 = 0;
if (dP1P2 > dP1P3) {
for (var y = p1.y >> 0; y <= p3.y >> 0; y++)
{
data.currentY = y;
if (y < p2.y) {
data.ndotla = nl1;
data.ndotlb = nl3;
data.ndotlc = nl1;
data.ndotld = nl2;
this.processScanLine(data, v1, v3, v1, v2, color);
}
else {
data.ndotla = nl1;
data.ndotlb = nl3;
data.ndotlc = nl2;
data.ndotld = nl3;
this.processScanLine(data, v1, v3, v2, v3, color);
}
}
}
else {
for (var y = p1.y >> 0; y <= p3.y >> 0; y++)
{
data.currentY = y;
if (y < p2.y) {
data.ndotla = nl1;
data.ndotlb = nl2;
data.ndotlc = nl1;
data.ndotld = nl3;
this.processScanLine(data, v1, v2, v1, v3, color);
}
else {
data.ndotla = nl2;
data.ndotlb = nl3;
data.ndotlc = nl1;
data.ndotld = nl3;
this.processScanLine(data, v2, v3, v1, v3, color);
}
}
}
}
【译者注:JavaScript 代码】
// 在两点之间从左往右画条线
// papb -> pcpd
// pa, pb, pc, pd 需要先进行排序
Device.prototype.processScanLine = function (data, va, vb, vc, vd, color) {
var pa = va.Coordinates;
var pb = vb.Coordinates;
var pc = vc.Coordinates;
var pd = vd.Coordinates;
// 由当前的 y 值,我们可以计算出梯度
// 以此再计算出 起始 X(sx) 和 结束 X(ex)
// 如果 pa.Y == pb.Y 或者 pc.Y== pd.y 的话,梯度强制为 1
var gradient1 = pa.y != pb.y ? (data.currentY - pa.y) / (pb.y - pa.y) : 1;
var gradient2 = pc.y != pd.y ? (data.currentY - pc.y) / (pd.y - pc.y) : 1;
var sx = this.interpolate(pa.x, pb.x, gradient1) >> 0;
var ex = this.interpolate(pc.x, pd.x, gradient2) >> 0;
// 开始 Z 值和结束 Z 值
var z1 = this.interpolate(pa.z, pb.z, gradient1);
var z2 = this.interpolate(pc.z, pd.z, gradient2);
var snl = this.interpolate(data.ndotla, data.ndotlb, gradient1);
var enl = this.interpolate(data.ndotlc, data.ndotld, gradient2);
// 从左(sx) 向右(ex) 绘制一条线
for (var x = sx; x < ex; x++) {
var gradient = (x - sx) / (ex - sx);
var z = this.interpolate(z1, z2, gradient);
var ndotl = this.interpolate(snl, enl, gradient);
// 使用光的向量和法线向量之间的角度余弦来改变颜色值
this.drawPoint(new BABYLON.Vector3(x, data.currentY, z),
new BABYLON.Color4(color.r * ndotl, color.g * ndotl, color.b * ndotl, 1));
}
};
Device.prototype.drawTriangle = function (v1, v2, v3, color) {
// 进行排序,p1 总在最上面,p2 总在最中间,p3 总在最下面
if (v1.Coordinates.y > v2.Coordinates.y) {
var temp = v2;
v2 = v1;
v1 = temp;
}
if (v2.Coordinates.y > v3.Coordinates.y) {
var temp = v2;
v2 = v3;
v3 = temp;
}
if (v1.Coordinates.y > v2.Coordinates.y) {
var temp = v2;
v2 = v1;
v1 = temp;
}
var p1 = v1.Coordinates;
var p2 = v2.Coordinates;
var p3 = v3.Coordinates;
// 光照位置
var lightPos = new BABYLON.Vector3(0, 10, 10);
// 计算光向量和法线向量之间夹角的余弦
// 它会返回介于 0 和 1 之间的值,该值将被用作颜色的亮度
var nl1 = this.computeNDotL(v1.WorldCoordinates, v1.Normal, lightPos);
var nl2 = this.computeNDotL(v2.WorldCoordinates, v2.Normal, lightPos);
var nl3 = this.computeNDotL(v3.WorldCoordinates, v3.Normal, lightPos);
var data = {};
// 计算线条的方向
var dP1P2;
var dP1P3;
// http://en.wikipedia.org/wiki/Slope
// 计算斜率
if (p2.y - p1.y > 0)
dP1P2 = (p2.x - p1.x) / (p2.y - p1.y); else
dP1P2 = 0;
if (p3.y - p1.y > 0)
dP1P3 = (p3.x - p1.x) / (p3.y - p1.y); else
dP1P3 = 0;
if (dP1P2 > dP1P3) {
for (var y = p1.y >> 0; y <= p3.y >> 0; y++) {
data.currentY = y;
if (y < p2.y) {
data.ndotla = nl1;
data.ndotlb = nl3;
data.ndotlc = nl1;
data.ndotld = nl2;
this.processScanLine(data, v1, v3, v1, v2, color);
} else {
data.ndotla = nl1;
data.ndotlb = nl3;
data.ndotlc = nl2;
data.ndotld = nl3;
this.processScanLine(data, v1, v3, v2, v3, color);
}
}
}
else {
for (var y = p1.y >> 0; y <= p3.y >> 0; y++) {
data.currentY = y;
if (y < p2.y) {
data.ndotla = nl1;
data.ndotlb = nl2;
data.ndotlc = nl1;
data.ndotld = nl3;
this.processScanLine(data, v1, v2, v1, v3, color);
} else {
data.ndotla = nl2;
data.ndotlb = nl3;
data.ndotlc = nl1;
data.ndotld = nl3;
this.processScanLine(data, v2, v3, v1, v3, color);
}
}
}
};
在浏览器中查看结果,请点击下面的截图:
3D 软件渲染引擎: 使用 Html5 在你的浏览器中查看高氏着色示例
你将会看到,性能/FPS 几乎相同,与平面着色算法相比,你将有一个更加美好的渲染效果。另外有一个更好的算法名为 Phong 着色算法。这里有另外一个使用 Html5 在浏览器中的测试场景,它使用了 Blender 导出的一个圆环形模型:
3D 软件渲染引擎: 查看圆环模型使用高氏着色的示例
你可以在这里下载执行这一高氏着色解决方案:
TypeScript: SoftEngineTSPart5GouraudShading.zip
- JavaScript: SoftEngineJSPart5GouraudShading.zip
在 下一个,也是最终教程 中,我们将看到应用了材质的模型,他看起来就像是这样:
而且我们也将看到一个使用 WebGL 引擎实现的完全相同的 3D 对象。然后,你就会明白为什么 GPU 是如此的重要,以提高实时 3D 渲染的表现!
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论