本文是在上一篇文章的框架上进一步将模型通用化,将原先的渲染对象从球体改成了三角面片,也进一步提高了难度,同时使用了ZBuffer解决遮挡问题。关于多边形的绘制可看这里
1、渲染场景如下图
2、实现过程,在此不再赘述一些变量的声明,如果发现编译过程中缺少了某些变量的定义,可以参照上一篇进行定义即可
(1)这次我们使用了ZBuffer,最后还想把ZBuffer显示出来,所以将图像的宽度扩大一倍以用于显示ZBuffer。因为我们是看向Z轴正方向的(这点和Games101不一样),所以ZBuffer初始化成一个很大的值
// 显示平面在Z=0的XY平面上,使用平行投影
_mainMatImg = cv::Mat(500, 500 * 2, CV_8UC3);
// 显示背景颜色
const unsigned char* bkColor = _renderParamWin->colors[0];
// 创建ZBuffer,并初始化,显示值小的元素
double* ZBuffer = new double[_mainMatImg.rows * _mainMatImg.cols / 2];
bool ifUseZBuffer = _renderParamWin->_ifUseZBuffer;
for (int y = 0; y < _mainMatImg.rows; ++y)
{
for (int x = 0; x < _mainMatImg.cols / 2; ++x)
{
// 先将其填成背景色
_mainMatImg.data[_mainMatImg.step * y + x * _mainMatImg.channels() + 0] = bkColor[2];
_mainMatImg.data[_mainMatImg.step * y + x * _mainMatImg.channels() + 1] = bkColor[1];
_mainMatImg.data[_mainMatImg.step * y + x * _mainMatImg.channels() + 2] = bkColor[0];
ZBuffer[y * _mainMatImg.cols / 2 + x] = 1e10;
}
}
(2)定义三棱锥的顶点坐标,以及4个面(都是三角形)对应的顶点索引
double pts[4][3] = // 定义三角锥4个顶点的坐标
{
{275, 103, 300},
{118, 490, 260},
{489, 319, 320},
{339, 279, 100}
};
int trigPtIdx[4][3] = // 三角锥4个三角形面3个顶点的索引
{
{0, 1, 3},
{1, 2, 3},
{2, 0, 3},
{0, 1, 2}
};
(3)同样的填充每个三角形对应的屏幕区域,这里先获取每个三角形的外接框,以减少计算量
// 计算搜寻范围
double b[4]; // 限定计算范围:xmin, xmax, ymin, ymax
//
int ptCnt;
double v1[2], v2[2], vxvRst;
bool ifNegative;
for (int triI = 0; triI < 4; ++triI) // 遍历4个三角形面
{
// 先求取该三角形的法向量
double normal[3] =
{
(pts[trigPtIdx[triI][1]][1] - pts[trigPtIdx[triI][0]][1])* (pts[trigPtIdx[triI][2]][2] - pts[trigPtIdx[triI][0]][2]) - (pts[trigPtIdx[triI][1]][2] - pts[trigPtIdx[triI][0]][2])* (pts[trigPtIdx[triI][2]][1] - pts[trigPtIdx[triI][0]][1]),
(pts[trigPtIdx[triI][1]][2] - pts[trigPtIdx[triI][0]][2])* (pts[trigPtIdx[triI][2]][0] - pts[trigPtIdx[triI][0]][0]) - (pts[trigPtIdx[triI][1]][0] - pts[trigPtIdx[triI][0]][0])* (pts[trigPtIdx[triI][2]][2] - pts[trigPtIdx[triI][0]][2]),
(pts[trigPtIdx[triI][1]][0] - pts[trigPtIdx[triI][0]][0])* (pts[trigPtIdx[triI][2]][1] - pts[trigPtIdx[triI][0]][1]) - (pts[trigPtIdx[triI][1]][1] - pts[trigPtIdx[triI][0]][1])* (pts[trigPtIdx[triI][2]][0] - pts[trigPtIdx[triI][0]][0])
};
// 求三角形的外包围盒
b[0] = _mainMatImg.cols;
b[1] = 0;
b[2] = _mainMatImg.rows;
b[3] = 0;
for (int ptI = 0; ptI < 3; ++ptI)
{
if (b[0] > pts[trigPtIdx[triI][ptI]][0] && pts[trigPtIdx[triI][ptI]][0] > 0) b[0] = pts[trigPtIdx[triI][ptI]][0];
if (b[1] < pts[trigPtIdx[triI][ptI]][0] && pts[trigPtIdx[triI][ptI]][0] < _mainMatImg.cols) b[1] = pts[trigPtIdx[triI][ptI]][0];
if (b[2] > pts[trigPtIdx[triI][ptI]][1] && pts[trigPtIdx[triI][ptI]][1] > 0) b[2] = pts[trigPtIdx[triI][ptI]][1];
if (b[3] < pts[trigPtIdx[triI][ptI]][1] && pts[trigPtIdx[triI][ptI]][1] < _mainMatImg.rows) b[3] = pts[trigPtIdx[triI][ptI]][1];
}
// 填充包围盒范围内的像素
for (int y = b[2]; y < b[3]; ++y)
{
for (int x = b[0]; x < b[1]; ++x)
{
// 遍历三角形的三个顶点,判别当前点是否在三角形内,其原理即使通过计算当前点到任意相邻两个点组成的向量的叉乘结果是否一致(同正或同负)
for (ptCnt = 0; ptCnt < 3; ++ptCnt)
{
// 求三角形边向量
v1[0] = pts[trigPtIdx[triI][(ptCnt + 1) % 3]][0] - pts[trigPtIdx[triI][ptCnt]][0];
v1[1] = pts[trigPtIdx[triI][(ptCnt + 1) % 3]][1] - pts[trigPtIdx[triI][ptCnt]][1];
// 求当前点和三角形起始点的向量
v2[0] = x - pts[trigPtIdx[triI][ptCnt]][0];
v2[1] = y - pts[trigPtIdx[triI][ptCnt]][1];
// 将上述两个向量叉乘以求方向
vxvRst = v1[0] * v2[1] - v1[1] * v2[0];
//
if (ptCnt == 0)
{
ifNegative = (vxvRst > 0 ? false : true);
}
else
{
if (ifNegative == true && vxvRst > 0
|| ifNegative == false && vxvRst < 0)
{
break;
}
}
}
// 如果该点在三角形内,就计算光照效果
if (ptCnt >= 3)
{
// 计算光照效果==================
LSum = 0;
// 1、计算环境光的效果
La = Ka;
LSum += La;
// 2、计算漫反射
// 计算三角形上任意(x,y)对应的z坐标,依据是:三角形平面法向量和三角形平面中的任意向量垂直,点乘乘积为0
double z = normal[0] * (pts[trigPtIdx[triI][0]][0] - x) + normal[1] * (pts[trigPtIdx[triI][0]][1] - y);
z /= normal[2];
z += pts[trigPtIdx[triI][0]][2];
// 如果Z坐标大于ZBuff中的值,说明被遮挡了
if (ifUseZBuffer == true && z > ZBuffer[y * _mainMatImg.cols / 2 + x])
{
continue;
}
ZBuffer[y * _mainMatImg.cols / 2 + x] = z;
// 计算点乘
Ld = normal[0] * (lightPos[0] - x) + normal[1] * (lightPos[1] - y) + normal[2] * (lightPos[2] - z);
if (Ld < 0)
{
Ld = 0;
}
// 计算夹角
Ld /= (sqrt(normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]) * sqrt(pow(x - lightPos[0], 2) + pow(y - lightPos[1], 2) + pow(z - lightPos[2], 2)));
Ld *= Kd;
LSum += Ld;
// 3、计算镜面反射
// 计算光线和观察点([0,0,0])的平分线向量
normalLightAndEye[0] = (x - x) + (lightPos[0] - x);
normalLightAndEye[1] = (y - y) + (lightPos[1] - y);
normalLightAndEye[2] = (0 - z) + (lightPos[2] - z);
Ls = normalLightAndEye[0] * normal[0] + normalLightAndEye[1] * normal[1] + normalLightAndEye[2] * normal[2];
Ls /= sqrt(pow(normalLightAndEye[0], 2) + pow(normalLightAndEye[1], 2) + pow(normalLightAndEye[2], 2));
Ls /= sqrt(pow(normal[0], 2) + pow(normal[1], 2) + pow(normal[2], 2));
if (Ls < 0)
{
Ls = 0;
}
// 将Ls进行指数级乘,此处为128次方处理,缩小高光区域
for (int i = 0; i < _renderParamWin->_k_Specular2; ++i)
{
Ls *= Ls;
}
Ls *= Ks;
LSum += Ls;
// 计算光照效果=================
_mainMatImg.data[y * _mainMatImg.step + x * _mainMatImg.channels() + 0] = ballColor[2] * LSum * lightColor[2] / 255;
_mainMatImg.data[y * _mainMatImg.step + x * _mainMatImg.channels() + 1] = ballColor[1] * LSum * lightColor[1] / 255;
_mainMatImg.data[y * _mainMatImg.step + x * _mainMatImg.channels() + 2] = ballColor[0] * LSum * lightColor[0] / 255;
}
}
}
}
3、渲染效果如图(这里有稍微调整过光照方向的,可能你们的结果会和我有点光影上的差异)
4、补充说明
(1)核心代码,即光照的计算过程和上一篇一模一样,唯一的不同点在于(x,y)对应的Z坐标的计算,详见代码中的注释;
(2)关于判别一个像素是否在三角形中可参照Games101的讲解,主要用到的是向量的叉乘结果,在此不赘述;
(3)三棱锥有什么好处?三棱锥由4个三角面组成,在图形学上,三角面是属于通用的结构,很多复杂的结构都是由三角面组成,其通用性比球体强太多了,如果4棱锥我们都能渲染,那么只要稍加修改,扩展到更复杂的3维结构上将是水到渠成;例如可以复杂一点的像这种
,甚至是这种
(4)关于ZBuffer,上一篇中因为球体的模型较为简单,所以我们当时将Z值计算简单化,取其小值,但此处的三角面片渲染场景较为复杂,所以需要用到ZBuffer,其用法也相当简单,就是不断的刷新其最小值即可,这个例子中如果不用ZBuffer,其渲染结果如下。
可以看到只能看见三棱锥的“底面”,原因很简单,底面三角形的数据在数组的最后面,也就是最后渲染,所以会将其他三角面的渲染结果重置掉。该例子较为简单,有兴趣的也可以修改一下代码,使其不用ZBuffer也能正确渲染。
我们来看一下最终ZBuffer的结果,可以看到越靠近顶点的位置值越小(也就越黑)。这里有个问题,为什么背景也是黑的?我也解答一下吧,那是因为其值过大,导致溢出产生的,反正不影响观看效果,就暂时不管了。
5、致谢
感谢闫令琪老师和Games101!