博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
[软件渲染器入门]五-平面着色和高氏着色
阅读量:7218 次
发布时间:2019-06-29

本文共 28232 字,大约阅读时间需要 94 分钟。

译者前言:

本文译自。原作者为,文章中假设有我的额外说明。我会加上【译者注:】。

正文開始:

这可能是整个系列中最优秀的部分:怎样处理光照

在之前。我们已经搞定了让每一个面随机显示一种颜色。如今我们要进行改变,计算出光的角度,让每一个面有更好的光照效果。第一种方法叫做平面着色。它使用面法线,用这种方法我们也会看到不同面的效果。可是高氏着色则会让我们更进一步。它使用顶点法线,然后每一个像素使用3个法线进行插值计算颜色。

在本教程的最后。你应该能够得到这样一个很酷的渲染效果:

本章教程是下面系列的一部分:

5 – 使用平面着色和高氏着色处理光 (本文)

平面着色

概念

为了可以应用平面着色算法,我们首先须要计算面的法线向量。我们一旦得到了它。我们还须要知道该法线向量和光向量之间的角度。

为了更精确。我们将使用返回给我们两个向量之间角的余弦。

由于这种值可能是-1和1之间的数。我们将它们收紧到0-1之间。我们的面依据终于的光量值来计算颜色。

总之。我们的面终于颜色将是 = color * Math.Max(0, cos(angle))

让我们从法线向量開始。维基百科定义指出:“对于(如)。一个表面法线可被计算为多边形两(非平行)边向量的”。

为了说明这一点。你能够在Blender文档中看到一个有趣的内容:

蓝色箭头是面的法线。绿色和红色箭头可能是面的不论什么边缘向量。让我们用Blender的苏珊妮模型来了解这些法线向量。

打开Blender。载入苏珊妮网格,切换到“编辑模式”:

编辑模式

通过点击它,然后按下“N”键打开网格的属性。

在“显示网格”中。你能找到2个法线相关button。点击“显示面的法线”:

网格属性

你将会得到类似这种效果:

效果

我们之后将会定义一个光。

这些光将成为教程中最简单的一个:一个点光源。这个点光源是简单的3D点(Vector3类型)。不管距离怎样,我们的面接受光的数量是同样的。然后,我们将会简单的基于法线向量和光点向量的角度以及我们的面的中心来改变光的强度。

因此,光的方向将是:光的位置 - 面的中心位置 -> 这将会给我们光的方向向量。

为了计算光向量和法线向量之间的角度,我们将使用点积:

光的方向

该图来自:(由John Chapman撰写的文章)

代码

普通情况下,我们将首先须要计算法线向量。幸运的是,Blender将为我们计算这些法线向量。更妙的是,它输出的每一个顶点的法线。我们将在第二部分使用。

因此,要计算我们的法线向量,我们仅仅须要取3个顶点的法线向量,将他们累加后除以3。

我们须要重构一下曾经的代码,一遍可以处理这些新的概念。

到如今为止,我们仅仅用到了Vector3类型的顶点数组。这已经不够了。我们还须要很多其它的数据:与顶点相关的法线(对于高氏着色而言)以及3D投影坐标。

实际上,当前投影仅仅在2D完毕。我们须要保持3D坐标投影才可以算出3D世界中的各种向量。

然后。我们将创建一个包括3个Vector3类型的结构:法线向量到顶点以及世界坐标,这些坐标是我们眼下一直在使用的。

这个ProcessScanLine方法必须进行插值很多其它的数据(比方高氏着色中每一个顶点的法线)。

因此,我们将创建一个ScanLineData结构。

【译者注:C#代码】

public class Mesh{    public string Name { get; set; }    public Vertex[] Vertices { get; private set; }    public Face[] Faces { get; set; }    public Vector3 Position { get; set; }    public Vector3 Rotation { get; set; }    public Mesh(string name, int verticesCount, int facesCount)    {        Vertices = new Vertex[verticesCount];        Faces = new Face[facesCount];        Name = name;    }}public struct Vertex{    public Vector3 Normal;    public Vector3 Coordinates;    public Vector3 WorldCoordinates;}
public struct ScanLineData{    public int currentY;    public float ndotla;    public float ndotlb;    public float ndotlc;    public float ndotld;}

【译者注:TypeScript代码】

export interface Vertex {    Normal: BABYLON.Vector3;    Coordinates: BABYLON.Vector3;    WorldCoordinates: BABYLON.Vector3;}export class Mesh {    Position: BABYLON.Vector3;    Rotation: BABYLON.Vector3;    Vertices: Vertex[];    Faces: Face[];    constructor(public name: string, verticesCount: number, facesCount: number) {        this.Vertices = new Array(verticesCount);        this.Faces = new Array(facesCount);        this.Rotation = new BABYLON.Vector3(0, 0, 0);        this.Position = new BABYLON.Vector3(0, 0, 0);    }}export interface ScanLineData {    currentY?: number;    ndotla?: number;    ndotlb?: number;    ndotlc?: number;    ndotld?: number;}
JavaScript代码与之前教程中的代码没有变化。因此我们不用改变什么。

除了进行结构改动。第一种是通过Blender导出的Json文件,我们须要载入的每一个顶点的法线以及建立顶点对象,而不是顶点数组中的Vector3类型的对象:

【译者注:C#代码】

// 首先填充我们网格的顶点数组for (var index = 0; index < verticesCount; index++){    var x = (float)verticesArray[index * verticesStep].Value;    var y = (float)verticesArray[index * verticesStep + 1].Value;    var z = (float)verticesArray[index * verticesStep + 2].Value;    // 载入Blender导出的顶点法线    var nx = (float)verticesArray[index * verticesStep + 3].Value;    var ny = (float)verticesArray[index * verticesStep + 4].Value;    var nz = (float)verticesArray[index * verticesStep + 5].Value;    mesh.Vertices[index] = new Vertex{ Coordinates= new Vector3(x, y, z), Normal= new Vector3(nx, ny, nz) };}
【译者注:TypeScript代码】

// 首先填充我们网格的顶点数组for (var index = 0; index < verticesCount; index++) {    var x = verticesArray[index * verticesStep];    var y = verticesArray[index * verticesStep + 1];    var z = verticesArray[index * verticesStep + 2];    // 载入Blender导出的顶点法线    var nx = verticesArray[index * verticesStep + 3];    var ny = verticesArray[index * verticesStep + 4];    var nz = verticesArray[index * verticesStep + 5];    mesh.Vertices[index] = {        Coordinates: new BABYLON.Vector3(x, y, z),        Normal: new BABYLON.Vector3(nx, ny, nz),        WorldCoordinates: null    };}
【译者注:JavaScript代码】

// 首先填充我们网格的顶点数组for (var index = 0; index < verticesCount; index++) {    var x = verticesArray[index * verticesStep];    var y = verticesArray[index * verticesStep + 1];    var z = verticesArray[index * verticesStep + 2];    // 载入Blender导出的顶点法线    var nx = verticesArray[index * verticesStep + 3];    var ny = verticesArray[index * verticesStep + 4];    var nz = verticesArray[index * verticesStep + 5];    mesh.Vertices[index] = {        Coordinates: new BABYLON.Vector3(x, y, z),        Normal: new BABYLON.Vector3(nx, ny, nz),        WorldCoordinates: null    };}
这里是全部已更新的方法/功能:

- Project() 在正在工作的顶点结构中。投射(使用世界矩阵)顶点的三维坐标。使得每一个顶点被正常投射。

DrawTriangle() 输入一些顶点结构,调用 NDotL 与 ComputeNDotL 算出结果,然后用这些数据调用 ProcessScanLine 函数。

ComputeNDotL() 计算法线和光的方向之间角度的余弦。

ProcessScanLine() 使用NDotL值改变颜色并发送到DrawTriangle。我们眼下每一个三角形仅仅有1种颜色。由于我们使用的是平面渲染。

假设你已经对之前的教程消化完成而且理解了本章开头的概念,那么你仅仅须要阅读以下的代码就能知道有哪些改变:

【译者注:C#代码】

// 将三维坐标和变换矩阵转换成二维坐标public Vertex Project(Vertex vertex, Matrix transMat, Matrix world){    // 将坐标转换为二维空间    var point2d = Vector3.TransformCoordinate(vertex.Coordinates, transMat);    // 在三维世界中转换坐标和法线的顶点    var point3dWorld = Vector3.TransformCoordinate(vertex.Coordinates, world);    var normal3dWorld = Vector3.TransformCoordinate(vertex.Normal, world);    // 变换后的坐标起始点是坐标系的中心点        // 可是。在屏幕上。我们以左上角为起始点        // 我们须要又一次计算使他们的起始点变成左上角      var x = point2d.X * renderWidth + renderWidth / 2.0f;    var y = -point2d.Y * renderHeight + renderHeight / 2.0f;    return new Vertex    {        Coordinates = new Vector3(x, y, point2d.Z),        Normal = normal3dWorld,        WorldCoordinates = point3dWorld    };}// 在两点之间从左到右绘制一条线段  // 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);    // 从左(sx)向右(ex)绘制一条线     for (var x = sx; x < ex; x++)    {        float gradient = (x - sx) / (float)(ex - sx);        var z = Interpolate(z1, z2, gradient);        var ndotl = data.ndotla;        // 基于光向量和法线向量之间角度的余弦改变颜色值        DrawPoint(new Vector3(x, data.currentY, z), color * ndotl);    }}// 计算光向量和法线向量之间角度的余弦// 返回0到1之间的值float ComputeNDotL(Vector3 vertex, Vector3 normal, Vector3 lightPosition){    var lightDirection = lightPosition - vertex;    normal.Normalize();    lightDirection.Normalize();    return Math.Max(0, Vector3.Dot(normal, lightDirection));}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 vnFace = (v1.Normal + v2.Normal + v3.Normal) / 3;    Vector3 centerPoint = (v1.WorldCoordinates + v2.WorldCoordinates + v3.WorldCoordinates) / 3;    // 光照位置    Vector3 lightPos = new Vector3(0, 10, 10);    // 计算光向量和法线向量之间夹角的余弦    // 它会返回介于0和1之间的值。该值将被用作颜色的亮度    float ndotl = ComputeNDotL(centerPoint, vnFace, lightPos);    var data = new ScanLineData { ndotla = ndotl };    // 计算线条的方向    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;    // 在第一种情况下,三角形是这种:    // P1    // -    // --     // - -    // -  -    // -   - P2    // -  -    // - -    // -    // P3    if (dP1P2 > dP1P3)    {        for (var y = (int)p1.Y; y <= (int)p3.Y; y++)        {            data.currentY = y;            if (y < p2.Y)            {                ProcessScanLine(data, v1, v3, v1, v2, color);            }            else            {                ProcessScanLine(data, v1, v3, v2, v3, color);            }        }    }    // 在另外一种情况下,三角形是这种:    //       P1    //        -    //       --     //      - -    //     -  -    // P2 -   -     //     -  -    //      - -    //        -    //       P3    else    {        for (var y = (int)p1.Y; y <= (int)p3.Y; y++)        {            data.currentY = y;            if (y < p2.Y)            {                ProcessScanLine(data, v1, v2, v1, v3, color);            }            else            {                ProcessScanLine(data, v2, v3, v1, v3, color);            }        }    }}
【译者注:TypeScript代码】

// 将三维坐标和变换矩阵转换成二维坐标public project(vertex: Vertex,transMat: BABYLON.Matrix,world: BABYLON.Matrix): Vertex {	// 将坐标转换为二维空间	var point2d = BABYLON.Vector3.TransformCoordinates(vertex.Coordinates, transMat);	// 在三维世界中转换坐标和法线的顶点	var point3DWorld = BABYLON.Vector3.TransformCoordinates(vertex.Coordinates, world);	var normal3DWorld = BABYLON.Vector3.TransformCoordinates(vertex.Normal, world);	// 变换后的坐标起始点是坐标系的中心点    	// 可是。在屏幕上。我们以左上角为起始点    	// 我们须要又一次计算使他们的起始点变成左上角  	var x = point2d.x * this.workingWidth + this.workingWidth / 2.0;	var y = -point2d.y * this.workingHeight + this.workingHeight / 2.0;	return ({		Coordinates: new BABYLON.Vector3(x, y, point2d.z),		Normal: normal3DWorld,		WorldCoordinates: point3DWorld	});}// 在两点之间从左到右绘制一条线段  // 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); // 从左(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 = data.ndotla; // 基于光向量和法线向量之间角度的余弦改变颜色值 this.drawPoint(new BABYLON.Vector3(x, data.currentY, z), new BABYLON.Color4(color.r * ndotl, color.g * ndotl, color.b * ndotl, 1)); } } // 计算光向量和法线向量之间角度的余弦 // 返回0到1之间的值 public computeNDotL(vertex: BABYLON.Vector3, normal: BABYLON.Vector3, lightPosition: BABYLON.Vector3): number { var lightDirection = lightPosition.subtract(vertex); normal.normalize(); lightDirection.normalize(); return Math.max(0, BABYLON.Vector3.Dot(normal, lightDirection)); } 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 vnFace = (v1.Normal.add(v2.Normal.add(v3.Normal))).scale(1 / 3); var centerPoint = (v1.WorldCoordinates.add(v2.WorldCoordinates.add(v3.WorldCoordinates))).scale(1 / 3); // 光照位置 var lightPos = new BABYLON.Vector3(0, 10, 10); // 计算光向量和法线向量之间夹角的余弦 // 它会返回介于0和1之间的值,该值将被用作颜色的亮度 var ndotl = this.computeNDotL(centerPoint, vnFace, lightPos); var data: ScanLineData = { ndotla: ndotl }; // 计算线条的方向 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; // 在第一种情况下,三角形是这种: // P1 // - // -- // - - // - - // - - P2 // - - // - - // - // P3 if (dP1P2 > dP1P3) { for (var y = p1.y >> 0; y <= p3.y >> 0; y++) { data.currentY = y; if (y < p2.y) { this.processScanLine(data, v1, v3, v1, v2, color); } else { this.processScanLine(data, v1, v3, v2, v3, color); } } } // 在另外一种情况下,三角形是这种: // P1 // - // -- // - - // - - // P2 - - // - - // - - // - // P3 else { for (var y = p1.y >> 0; y <= p3.y >> 0; y++) { data.currentY = y; if (y < p2.y) { this.processScanLine(data, v1, v2, v1, v3, color); } else { this.processScanLine(data, v2, v3, v1, v3, color); } } } }

【译者注:JavaScript代码】

// 将三维坐标和变换矩阵转换成二维坐标Device.prototype.project = function (vertex, transMat, world) {    // 将坐标转换为二维空间    var point2d = BABYLON.Vector3.TransformCoordinates(vertex.Coordinates, transMat);    // 在三维世界中转换坐标和法线的顶点    var point3DWorld = BABYLON.Vector3.TransformCoordinates(vertex.Coordinates, world);    var normal3DWorld = BABYLON.Vector3.TransformCoordinates(vertex.Normal, world);    // 变换后的坐标起始点是坐标系的中心点        // 可是。在屏幕上,我们以左上角为起始点        // 我们须要又一次计算使他们的起始点变成左上角      var x = point2d.x * this.workingWidth + this.workingWidth / 2.0;    var y = -point2d.y * this.workingHeight + this.workingHeight / 2.0;    return ({        Coordinates: new BABYLON.Vector3(x, y, point2d.z),        Normal: normal3DWorld,        WorldCoordinates: point3DWorld    });};// 在两点之间从左到右绘制一条线段  // 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); // 从左(sx)向右(ex)绘制一条线 for (var x = sx; x < ex; x++) { var gradient = (x - sx) / (ex - sx); var z = this.interpolate(z1, z2, gradient); var ndotl = data.ndotla; // 基于光向量和法线向量之间角度的余弦改变颜色值 this.drawPoint(new BABYLON.Vector3(x, data.currentY, z), new BABYLON.Color4(color.r * ndotl, color.g * ndotl, color.b * ndotl, 1)); } }; // 计算光向量和法线向量之间角度的余弦 // 返回0到1之间的值 Device.prototype.computeNDotL = function (vertex, normal, lightPosition) { var lightDirection = lightPosition.subtract(vertex); normal.normalize(); lightDirection.normalize(); return Math.max(0, BABYLON.Vector3.Dot(normal, lightDirection)); }; 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 vnFace = (v1.Normal.add(v2.Normal.add(v3.Normal))).scale(1 / 3); var centerPoint = (v1.WorldCoordinates.add(v2.WorldCoordinates.add(v3.WorldCoordinates))).scale(1 / 3); // 光照位置 var lightPos = new BABYLON.Vector3(0, 10, 10); // 计算光向量和法线向量之间夹角的余弦 // 它会返回介于0和1之间的值,该值将被用作颜色的亮度 var ndotl = this.computeNDotL(centerPoint, vnFace, lightPos); var data = { ndotla: ndotl }; // 计算线条的方向 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; // 在第一种情况下,三角形是这种: // P1 // - // -- // - - // - - // - - P2 // - - // - - // - // P3 if (dP1P2 > dP1P3) { for (var y = p1.y >> 0; y <= p3.y >> 0; y++) { data.currentY = y; if (y < p2.y) { this.processScanLine(data, v1, v3, v1, v2, color); } else { this.processScanLine(data, v1, v3, v2, v3, color); } } } // 在另外一种情况下,三角形是这种: // P1 // - // -- // - - // - - // P2 - - // - - // - - // - // P3 else { for (var y = p1.y >> 0; y <= p3.y >> 0; y++) { data.currentY = y; if (y < p2.y) { this.processScanLine(data, v1, v2, v1, v3, color); } else { this.processScanLine(data, v2, v3, v1, v3, color); } } } };

要查看浏览器中的效果。请点击以下的截图:

3D软件渲染引擎:

在我的联想X1 Carbon (酷睿i7 lvy Bridge)中。使用 Internet Explorer 11 (这似乎是我的Windows8.1机器中最快的浏览器) 我跑这个640x480的实现大约能够跑到 35FPS。而且在 Surface RT 中大约能够得到 4FPS 每秒的执行速度。C#的并行版本号渲染相同的场景则能够执行在 60FPS速度下。

你能够在这里下载运行这一平面渲染解决方式:

- C#:

- TypeScript:

- JavaScript:

高氏着色

概念

以假设你已经成功的理解了平面着色,那么你会发现高氏着色并不复杂。这次我们不仅针对每一个面赋予一个颜色。而是依据三角形的顶点使用3个法线。

然后我们定义颜色的3个级别,使用插值在之前的教程中使用同样的算法对每一个顶点之间的像素赋予颜色。使用这样的插值,我们将得到三角形连续的光影效果。

法线向量

图片摘取自:

你能够在这张图中看出平面着色和高氏着色的差别。平面着色採用了居中的独有法线。高氏着色则使用了3个顶点法线。

你还能够看看3D网格(棱锥),法线是每顶点每面。我的意思是同样的顶点将具有基于我们当前绘制面不同的法线。

让我们回到绘制三角面逻辑中来。

有一个非常好的方式来说明我们要做的阴影:

三角形顶点线性插值说明

摘自:(作者:Ben Cloward)

在该图中,如果上方顶点有一个>90度夹角的光的方向。它的颜色应该是黑色的(光的最小级别 = 0)。

想象一下如今的其它两个顶点法线与光的方向角度为0度,这意味着他们应受到光的最大级别(1)。

为了填充我们的三角形,我们还须要用到插值来使每一个顶点之间的颜色有一个非常好的过渡。

实现代码

由于代码很easy。稍作阅读就行理解我实现的颜色插值了。

【译者注: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软件渲染引擎:

你将会看到。性能/FPS差点儿同样,与平面着色算法相比。你将有一个更加美好的渲染效果。另外有一个更好的算法名为Phong着色算法。

这里有另外一个使用Html5在浏览器中的測试场景,它使用了Blender导出的一个圆环形模型:

3D软件渲染引擎:

你能够在这里下载运行这一高氏着色解决方式:

- C#: 

- TypeScript: 

- JavaScript: 

在中。我们将看到应用了材质的模型,他看起来就像是这样:

终于章预览图

并且我们也将看到一个使用WebGL引擎实现的全然同样的3D对象。然后,你就会明确为什么GPU是如此的重要,以提高实时3D渲染的表现!

你可能感兴趣的文章
IP釋放、清除、以及刷新DNS
查看>>
第二次作业
查看>>
小知识
查看>>
安装Vmware时竟然也会报错,错误信息见图
查看>>
20179311《网络攻防实践》第三周作业
查看>>
Ural 1042 Central Heating
查看>>
css兼容问题大全
查看>>
2018-2019-1 20165324《信息安全系统设计基础》实验五
查看>>
使用 Applet 渲染 jzy3d WireSurface 波动率曲面图
查看>>
9 Web开发——springmvc自动配置原理
查看>>
截取图片
查看>>
Python学习--01入门
查看>>
MySQL联合查询语法内联、左联、右联、全联
查看>>
看牛顿法的改进与验证局部收敛
查看>>
第十篇、自定义UIBarButtonItem和UIButton block回调
查看>>
复分析学习1
查看>>
Java虚拟机笔记(四):垃圾收集器
查看>>
计算机运行命令全集
查看>>
WebSocket 实战
查看>>
二次排序
查看>>