之前我们为每一个面随机赋予颜色,然而在渲染场景时,为了贴合现实世界,还需要在渲染中加入明暗的对比。在这我们不仅要实现黑白变化还有每个面受到光照的影响后产生的亮度变化。
平面着色
现在先介绍简单的平面着色。它会计算光照的角度,使每个面有更好的光照效果,但每个面都会被指定一个单一且没有变化的颜色。这种方法虽然会产生出不真实的效果,但相比于没有明暗变化已经进步了。
计算每一个面的法线向量,由此可以知道该法线向量与光向量之间的角度。
依据线性代数,我们可以借助向量点乘积来获得两向量之间的夹角angle关系。
Vector_1 * Vector_2 = | Vector_1| * |Vector_2| * cos< angle >
在这里我们计算得到的是cos< angle>,它的值分布在[-1,1],我们归一化至[0,1],从而直接利用这个值来设置我们的每个面的颜色。
Surface_color = color * Max(0, cos(angle) )
代码这里我做了范围限制,为了防止浮点运算超出(-1,+1)
其中明显看出舍弃了cos(angle)负值部分,因为负值代表该着色面位于背光侧。
以下给出部分被修改的关键代码。
voidProcessScanLine(ScanLineData &scld, Vector4D &pa, Vector4D &pb, Vector4D &pc, Vector4D &pd, UINT32& color) {
;;;//其他部分略
Color color_s;
//画出扫描线
for (int x = sx; x <=ex;x++) {
float gradient = (x - sx)/ (float)(ex - sx); //1.插值该点深度
float z = INTERP(z1, z2, gradient);
//float ndotl = (snl + (enl - snl) * gradient);
//float z = (-D - A*x - B*scld.currentY)*C_inv; //2.计算该点深度
// 基于光向量和法线向量之间角度的余弦改变颜色值
color_s.Set(color, scld.ndotla);
PutPixel(x,scld.currentY, z,color_s.uint32);
}
}
voidDrawTriangle(Vertexpa_v, Vertexpb_v, Vertexpc_v, UINT32color) {
//光栅化画三角形 使用光照
//首先将a,b,c按照行从小到大排列,且当pb与某一点位于同一水平线上时,使b在左侧
Vector4D tmp,pa,pb,pc;
Vertex tmp_v;
if (pa_v.Coordinates.y > pb_v.Coordinates.y) {
tmp_v=pa_v;
pa_v=pb_v;
pb_v= tmp_v;
}
if (pa_v.Coordinates.y > pc_v.Coordinates.y) {
tmp_v=pa_v;
pa_v=pc_v;
pc_v= tmp_v;
}
if (pb_v.Coordinates.y > pc_v.Coordinates.y) {
tmp_v=pb_v;
pb_v=pc_v;
pc_v= tmp_v;
}
pa=pa_v.Coordinates;
pb=pb_v.Coordinates;
pc=pc_v.Coordinates;
//计算三角形面的法向量 == 三个顶点法向量的平均值
Vector4D normal;
pa_v.Normal.Add(pb_v.Normal, normal);
normal.Add(pc_v.Normal, normal);
normal.Mul_float(0.3333f,normal);
//cout << normal.x << ' ' << normal.y<< ' ' << normal.z << endl;
//计算三顶点的中心位置(所代表面的中心位置)
Vector4D center_point;
pa_v.WorldCoordinates.Add(pb_v.WorldCoordinates,center_point);
center_point.Add(pc_v.WorldCoordinates,center_point);
center_point.Mul_float(0.3333f,center_point);
//光照位置
light_s.light_source.x= 0;
light_s.light_source.y= 10;
light_s.light_source.z= 0;
float ndot =light_s.plane_light_cos(center_point, normal);
//if(ndot!=0) cout << ndot << endl;
ScanLineData scld;
scld.ndotla= ndot;
//scld.ndotla = 1.0;
;;;//略
//Pa
// - Pb
// Pc
if (dPaPb > dPaPc)
{
for (int row = (int)pa.y; row <= (int)pc.y; row++)
{
if (row < pb.y)
{
scld.currentY= row;
ProcessScanLine(scld,pa, pc, pb, pa, color);
}
else
{
scld.currentY= row;
ProcessScanLine(scld,pa, pc, pb, pc, color);
}
}
}
}
运行时如下图(点光源在其左侧)
Gouraud Shading
平面着色主要运用了平面法向量与光线向量的夹角来实现明暗变化,易于理解,我们接下来学习使用更好的着色方法---Gouraud Shading(高氏着色)。
他根据三角形三个顶点的法矢量,和光线向量,得出这三点的光强。然后,沿三角形的边和水平扫描线分别进行插值计算,得出这个三角形上的各点的光强,从而对每个像素点进行着色,最终我们可以看见连续的光影效果--渐变。
从上面看出,平面着色采用了居中法线,高氏着色则使用了3个顶点法线。
注意,这里的顶点法线其实是该顶点所有邻面的法向量的平均值。
上述语言好像不是很直白,我们来总结下步骤。
1.计算出每个三角面的三个顶点1.2.3的法向量与光线向量的cos值(强度值)。
2.依据顶点1.2线性插值得到点4的强度值,依据顶点1.3得到点5强度值
3.依据4.5线性插值得到当前像素的强度值。
目标效果图
关键代码
voidDrawTriangleGouraud(Vertexpa_v, Vertexpb_v, Vertexpc_v, UINT32color) {
//光栅化画三角形 使用光照高氏着色
//首先将a,b,c按照行从小到大排列,且当pb与某一点位于同一水平线上时,使b在左侧
Vector4D tmp, pa, pb, pc;
Vertex tmp_v;
if (pa_v.Coordinates.y > pb_v.Coordinates.y) {
tmp_v=pa_v;
pa_v=pb_v;
pb_v= tmp_v;
}
if (pa_v.Coordinates.y > pc_v.Coordinates.y) {
tmp_v=pa_v;
pa_v=pc_v;
pc_v= tmp_v;
}
if (pb_v.Coordinates.y > pc_v.Coordinates.y) {
tmp_v=pb_v;
pb_v=pc_v;
pc_v= tmp_v;
}
pa=pa_v.Coordinates;
pb=pb_v.Coordinates;
pc=pc_v.Coordinates;
//光照位置
light_s.light_source.x= 0;
light_s.light_source.y= 10;
light_s.light_source.z= 0;
//每顶点的法向量与光线向量的cos
float ndot1 =light_s.plane_light_cos(pa_v.WorldCoordinates, pa_v.Normal);
float ndot2 =light_s.plane_light_cos(pb_v.WorldCoordinates, pb_v.Normal);
float ndot3 =light_s.plane_light_cos(pc_v.WorldCoordinates, pc_v.Normal);
ScanLineData scld;
//2种特殊
//平顶
if (pa.y == pb.y) {
if (pa.x < pb.x) {
tmp= pa;
pa= pb;
pb= tmp;
}
for (int row = (int)pa.y; row <= (int)pc.y; row++) {
scld.currentY= row;
scld.ndotla= ndot2;
scld.ndotlb= ndot3;
scld.ndotlc = ndot1;
scld.ndotld= ndot3;
ProcessScanLineGouraud(scld,pb, pc, pa, pc, color); //这里的顶点排列是顺序相关的
}
return;
}
//平底
if (pc.y == pb.y) {
if (pc.x < pb.x) {
tmp= pb;
pb= pc;
pc= tmp;
}
for (int row = (int)pa.y; row <= (int)pc.y; row++) {
scld.currentY= row;
scld.ndotla= ndot1;
scld.ndotlb= ndot2;
scld.ndotlc= ndot3;
scld.ndotld= ndot1;
ProcessScanLineGouraud(scld,pa, pb, pc, pa, color);
}
return;
}
float dPaPb, dPaPc;
if (pb.y - pa.y >0)
dPaPb= (pb.x - pa.x) / (pb.y - pa.y);
else
dPaPb= 0;
if (pc.y - pa.y >0)
dPaPc= (pc.x - pa.x) / (pc.y - pa.y);
else
dPaPc= 0;
// Pa
// - Pb
// Pc
if (dPaPb > dPaPc)
{
for (int row = (int)pa.y; row <= (int)pc.y; row++)
{
scld.currentY= row;
if (row < pb.y)
{
scld.ndotla= ndot1;
scld.ndotlb= ndot3;
scld.ndotlc= ndot2;
scld.ndotld= ndot1;
ProcessScanLineGouraud(scld,pa, pc, pb, pa, color);
}
else
{
scld.ndotla= ndot1;
scld.ndotlb= ndot3;
scld.ndotlc= ndot2;
scld.ndotld= ndot3;
ProcessScanLineGouraud(scld,pa, pc, pb, pc, color);
}
}
}
// Pa
//Pb -
// Pc
else
{ //dPaPb <= dPaPc
for (int row = (int)pa.y; row <= (int)pc.y; row++)
{
scld.currentY= row;
if (row < pb.y)
{
scld.ndotla= ndot1;
scld.ndotlb= ndot2;
scld.ndotlc= ndot3;
scld.ndotld= ndot1;
ProcessScanLineGouraud(scld,pa, pb, pc, pa, color);
}
else
{
scld.ndotla= ndot2;
scld.ndotlb= ndot3;
scld.ndotlc= ndot1;
scld.ndotld= ndot3;
ProcessScanLineGouraud(scld,pb, pc, pa, pc, color);
}
}
}
}
voidDrawTriangleGouraud(Vertexpa_v, Vertexpb_v, Vertexpc_v, UINT32color) {
//光栅化画三角形 使用光照高氏着色
//首先将a,b,c按照行从小到大排列,且当pb与某一点位于同一水平线上时,使b在左侧
Vector4D tmp, pa, pb, pc;
Vertex tmp_v;
if (pa_v.Coordinates.y > pb_v.Coordinates.y) {
tmp_v=pa_v;
pa_v=pb_v;
pb_v= tmp_v;
}
if (pa_v.Coordinates.y > pc_v.Coordinates.y) {
tmp_v=pa_v;
pa_v=pc_v;
pc_v= tmp_v;
}
if (pb_v.Coordinates.y > pc_v.Coordinates.y) {
tmp_v=pb_v;
pb_v=pc_v;
pc_v= tmp_v;
}
pa=pa_v.Coordinates;
pb=pb_v.Coordinates;
pc=pc_v.Coordinates;
//光照位置
light_s.light_source.x= 0;
light_s.light_source.y= 10;
light_s.light_source.z= 0;
//每顶点的法向量与光线向量的cos
float ndot1 =light_s.plane_light_cos(pa_v.WorldCoordinates, pa_v.Normal);
float ndot2 =light_s.plane_light_cos(pb_v.WorldCoordinates, pb_v.Normal);
float ndot3 =light_s.plane_light_cos(pc_v.WorldCoordinates, pc_v.Normal);
ScanLineData scld;
//2种特殊
//平顶
if (pa.y == pb.y) {
if (pa.x < pb.x) {
tmp= pa;
pa= pb;
pb= tmp;
}
for (int row = (int)pa.y; row <= (int)pc.y; row++) {
scld.currentY= row;
scld.ndotla= ndot2;
scld.ndotlb= ndot3;
scld.ndotlc= ndot1;
scld.ndotld= ndot3;
ProcessScanLineGouraud(scld,pb, pc, pa, pc, color); //这里的顶点排列是顺序相关的
}
return;
}
//平底
if (pc.y == pb.y) {
if (pc.x < pb.x) {
tmp= pb;
pb= pc;
pc= tmp;
}
for (int row = (int)pa.y; row <= (int)pc.y; row++) {
scld.currentY= row;
scld.ndotla= ndot1;
scld.ndotlb= ndot2;
scld.ndotlc= ndot3;
scld.ndotld= ndot1;
ProcessScanLineGouraud(scld,pa, pb, pc, pa, color);
}
return;
}
float dPaPb, dPaPc;
if (pb.y - pa.y >0)
dPaPb= (pb.x - pa.x) / (pb.y - pa.y);
else
dPaPb= 0;
if (pc.y - pa.y >0)
dPaPc= (pc.x - pa.x) / (pc.y - pa.y);
else
dPaPc= 0;
// Pa
// - Pb
// Pc
if (dPaPb > dPaPc)
{
for (int row = (int)pa.y; row <= (int)pc.y; row++)
{
scld.currentY= row;
if (row < pb.y)
{
scld.ndotla = ndot1;
scld.ndotlb= ndot3;
scld.ndotlc= ndot2;
scld.ndotld= ndot1;
ProcessScanLineGouraud(scld,pa, pc, pb, pa, color);
}
else
{
scld.ndotla= ndot1;
scld.ndotlb= ndot3;
scld.ndotlc= ndot2;
scld.ndotld= ndot3;
ProcessScanLineGouraud(scld,pa, pc, pb, pc, color);
}
}
}
// Pa
//Pb -
// Pc
else
{ //dPaPb <= dPaPc
for (int row = (int)pa.y; row <= (int)pc.y; row++)
{
scld.currentY= row;
if (row < pb.y)
{
scld.ndotla= ndot1;
scld.ndotlb= ndot2;
scld.ndotlc= ndot3;
scld.ndotld= ndot1;
ProcessScanLineGouraud(scld,pa, pb, pc, pa, color);
}
else
{
scld.ndotla= ndot2;
scld.ndotlb= ndot3;
scld.ndotlc= ndot1;
scld.ndotld= ndot3;
ProcessScanLineGouraud(scld,pb, pc, pa, pc, color);
}
}
}
}
最终我们的效果图(这是一个多边形球体,类似每个面为三角形时的C60)
ref:
wiki_shading:
https://en.wikipedia.org/wiki/Shading#Flat_shading
帮助理解法线:
https://en.wikibooks.org/wiki/Blender_3D:_Noob_to_Pro/Printable_Version#Normal_coordinates