最近学习了Games101,听到Blinn-Phong反射模型的时候就想着自己动手写一下,加深理解,于是,有了本文。
本文的代码避开OpenGL的门槛,直接使用C语言,最终展示结果的时候用了OpenCV,所以需要C的入门知识以及一个OpenCV的开发环境(没有也不影响代码的阅读和原理的理解)。
1、渲染场景如下,在坐标系的某个位置上有一个球体,如下图
2、实现过程
(1)首先,定义环境参数,包括球体的位置、大小、颜色,光照的位置、颜色等,当然还有Blinn-Phong模型中的几个参数:环境光系数、漫反射系数、镜面反射系数等
// 首先定义场景中存在一个球体,其球心和半径如下
double ballCenter[3] = { 250, 250, 500 };
double ballRadius = 150;
unsigned char ballColor[3] = { 255, 255, 0};
// 显示背景颜色
unsigned char bkColor[3] = { 0, 0, 0 };
// 定义光线的各类参数
double lightPos[3] = { 0, 0, 0}; // 光源位置,这里使用点光源(即锥形光),但是只要将光源距离拉的足够大,其实就是平行光源
unsigned char lightColor[3] = { 255, 255, 255 }; // 光的颜色
double La, Ka = 0.3; // 环境光系数和强度
double Ld, Kd = 0.5; // 漫反射系数
double Ls, Ks = 0.2; // 镜面反射系数
double LSum;
double normalLightAndEye[3];
(2)定义一张用于显示的OpenCV图片,
// 显示平面在Z=0的XY平面上,使用平行投影
_mainMatImg = cv::Mat(500, 500, CV_8UC3);
(3)接下来所做的工作就是将上一步的图片填完整即可,核心代码在两个for循环中执行
for (int y = 0; y < _mainMatImg.rows; ++y)
{
for (int x = 0; x < _mainMatImg.cols; ++x)
{
// 先将其填成背景色
_mainMatImg.data[_mainMatImg.step * y + x * _mainMatImg.channels() + 0] = bkColor[0];
_mainMatImg.data[_mainMatImg.step * y + x * _mainMatImg.channels() + 1] = bkColor[1];
_mainMatImg.data[_mainMatImg.step * y + x * _mainMatImg.channels() + 2] = bkColor[2];
if ((x - ballCenter[0]) * (x - ballCenter[0])
+ (y - ballCenter[1]) * (y - ballCenter[1]) > ballRadius * ballRadius)
{
continue;
}
LSum = 0;
// 1、计算环境光的效果
La = Ka;
LSum += La;
// 2、计算漫反射
double z = ballRadius * ballRadius - (x - ballCenter[0]) * (x - ballCenter[0])
- (y - ballCenter[1]) * (y - ballCenter[1]);
z = -sqrt(z) + ballCenter[2]; // 求得[x,y]坐标下球表面的z坐标
// 计算点乘
Ld = (x - ballCenter[0]) * (lightPos[0] - x) + (y - ballCenter[1]) * (lightPos[1] - y) + (z - ballCenter[2]) * (lightPos[2] - z);
if (Ld < 0)
{
Ld = 0;
}
// 计算夹角
Ld /= (ballRadius * sqrt(pow(x - lightPos[0], 2) + pow(y - lightPos[1], 2) + pow(z - lightPos[2], 2)));
Ld *= Kd;
LSum += Ld;
// 3、计算镜面反射
// 计算光线和观察点([0,x,y])的平分线向量
normalLightAndEye[0] = (x - x) + (lightPos[0] - x);
normalLightAndEye[1] = (y - y) + (lightPos[1] - y);
normalLightAndEye[2] = (0 - z) + (lightPos[2] - z);
Ls = normalLightAndEye[0] * (x - ballCenter[0]) + normalLightAndEye[1] * (y - ballCenter[1]) + normalLightAndEye[2] * (z - ballCenter[2]);
Ls /= sqrt(pow(normalLightAndEye[0], 2) + pow(normalLightAndEye[1], 2) + pow(normalLightAndEye[2], 2));
Ls /= sqrt(pow(x - ballCenter[0], 2) + pow(y - ballCenter[1], 2) + pow(z - ballCenter[2], 2));
if (Ls < 0)
{
Ls = 0;
}
// 将Ls进行指数级乘,此处为128次方处理,缩小高光区域
for (int i = 0; i < 7; ++i)
{
Ls *= Ls;
}
Ls *= Ks;
LSum += Ls;
//
_mainMatImg.data[_mainMatImg.step * y + x * _mainMatImg.channels() + 0] = ballColor[0] * LSum * lightColor[0] / 255;
_mainMatImg.data[_mainMatImg.step * y + x * _mainMatImg.channels() + 1] = ballColor[1] * LSum * lightColor[1] / 255;
_mainMatImg.data[_mainMatImg.step * y + x * _mainMatImg.channels() + 2] = ballColor[2] * LSum * lightColor[2] / 255;
}
}
(4)最后,将其展示出来即可
cv::imshow("Blinn-Phong Model", _mainMatImg);
3、显示结果如下
4、补充说明:
(1)实现的过程基于Games101中闫老师的介绍,想要看明白漫反射和镜面反射的计算,可能需要 一定的向量计算的知识,例如向量夹角的计算,已知入射光线和反射光线计算其夹角平分线的向量等。这部分知识可以从Games101中获取;
(2)在计算球体的Z坐标的时候,每一对(x,y)应该能求得符号相反的两个在z值,因为屏幕在Z=0位置上,球体在Z=500的位置上,所以只取其较小值,我们将在下一篇中使用ZBuffer解决这个问题
5、致谢
感谢闫令琪老师和Games101!