光照模型将采用广泛应用的phong模型,虽然这种模型在openGL的固定管线中已经实现了,但是学习该光照模型可以更加清楚的了解可编程渲染管线的流程。
首先要实现phong光照模型先要了解该模型中光照计算,关于phong模型光照计算的相关资料网上相当多,或者参考任何一本计算机图形学的相关书籍即可,这里只给出计算公式。
C = ambient + diffuse + specular
从公式中可以得出,物体顶点的最终颜色是由环境反射,漫反射和镜面反射三个成分来决定的。环境反射的计算公式为:
ambient = IaKa
其中,Ia是光的环境反射强度,Ka是物体材质的环境射系数。环境反射很简单,和光源、法线等等都没有关系,下面是只有环境反射的例子。
Fig1环境反射,Ia=(1.0, 1.0, 1.0),Ka=(0.15,0.15,0.0)
和环境反射相比,漫反射就稍微复杂一点,它和光源的位置和物体顶点的法线都有关系。漫反射的计算公式为:
diffuse = IdKd (NL)
其中Id是光的漫反射强度,Kd是物体材质漫反射系数,NL表示法线N和入射光线L的内积。下面是只有面反射的例子。
Fig2漫反射,Id=(1.0, 1.0, 1.0),Kd=(1.0, 1.0 , 0.0)
最后是镜面反射,和漫反射相比,镜面反射又复杂了一点。它不仅和光源位置,物体顶点法线有关系,而且还和我们观看的位置有关系。镜面反射的计算公式为:
specular = IsKs(VR)n
其中Is是光的镜面反射强度,Ks是物体材质的镜面反射系数,VR表示相机朝向向量V和反射光线R的内积,n表示该内积的n次幂。这里反射光线R可以通过公式
R = 2(LN)N-L
来得到。这里还可以使用half vector来计算,计算half vector要比计算反射向量R方便快速的多。
H=(L+V)/2
所以,镜面反射公式现在可以写成
specular = IsKs(NH)n
下面是只有镜面反射的例子。
Fig3镜面反射,Is=(1.0, 1.0, 1.0),Ks=(1.0, 1.0 , 1.0,), n=32
通过上面的过程,分别计算出了物体每个顶点的环境反射,面反射和镜面反射。最后简单将这三个成分相加即可得到顶点最终的颜色。
C = IaKa + IdKd (NL)+ IsKs(NH)n
图fig4显示了这个相加的过程。
Fig4 phong光照
下面是vertex shader的代码。由于是基于vertex的光照,所以不需要fragment shader。
vertex脚本:
uniform float3 LightPosition; //光源位置 uniform float3 eyePosition; //相机位置 uniform float3 I; //光强度 uniform float3 Ka; //环境光反射系数 uniform float3 Kd; //漫反射系数 uniform float3 Ks; //镜面反射系数 uniform float shininess; //n幂次 struct output { float4 position : POSITION; float4 color : COLOR; }; output v_main( float4 position : POSITION, float3 normal : NORMAL, uniform float4x4 MV, // 在相机坐标系中计算,所以要用到ModelView变换矩阵 uniform float4x4 MVP // ModelViewProjection变换矩阵 ) { output OUT; OUT.position = mul(MVP, position); float3 N = normalize(mul(MV, float4(normal,0.0)) ).xyz; //转换法线到相机坐标系 float3 P = mul(MV, position).xyz; //转换物体顶点到相机坐标系 float3 L = normalize(LightPosition - P); float NdotL = max(dot(N,L),0); //判断法线和入射光线的角度是否大于90度 float3 ambient = Ka * I; //环境反射 float3 diffuse = Kd * I * NdotL; //漫反射 float3 V = normalize(eyePosition - P); float3 H = normalize(L+V); //half vector float NdotH = pow(max(dot(N,H), 0), shininess); if(NdotL<=0) NdotH = 0.0; float3 specular = Ks*I*NdotH; //镜面反射 float3 color = ambient + diffuse +specular; //所有成分相加 OUT.color.xyz= color; OUT.color.w = 1.0; return OUT; }
主程序:
#include <gl/glut.h>
#include <cg/cg.h>
#include <Cg/cgGL.h>
#include <stdio.h>
#include <Windows.h>
int ww = 640, hh = 480;
void render();
void reshape(int w, int h);
static CGcontext myCgContext;
static CGprofile myCgVertexProfile;
static CGprogram myCgVertexProgram;
static const char *myProgramName = "Lighting CG";
static const char *myVertexProgramFileName = "CSDN_02v.cg";
static const char *myVertexProgramName = "v_main";
CGparameter lp, ep, i, a, d, s, n;
//display FPS on the title of the window
static float lastTime = 0.0f;
void displayFPS(){
static float framesPerSecond = 0.0f; // This will store our fps
// This will hold the time from the last frame
float currentTime = GetTickCount() * 0.001f;
if( currentTime - lastTime > 0.0f )
{
framesPerSecond = 1/(currentTime - lastTime);
char strFrameRate[256];
lastTime = currentTime;
sprintf(strFrameRate, "Current Frames Per Second: %f", framesPerSecond);
glutSetWindowTitle( strFrameRate);
framesPerSecond = 0;
}
}
int main(int argc, char** argv)
{
//【1】初始化部分
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE | GLUT_DEPTH);
glutInitWindowSize(ww,hh);
glutCreateWindow(myProgramName);
//【2】构建shader运行环境;
myCgContext = cgCreateContext();
cgGLSetDebugMode(CG_FALSE);
cgSetParameterSettingMode(myCgContext, CG_DEFERRED_PARAMETER_SETTING);
myCgVertexProfile = cgGLGetLatestProfile(CG_GL_VERTEX);
cgGLSetOptimalOptions(myCgVertexProfile);
myCgVertexProgram =
cgCreateProgramFromFile(
myCgContext, /* Cg runtime context */
CG_SOURCE, /* Program in human-readable form */
myVertexProgramFileName, /* Name of file containing program */
myCgVertexProfile, /* Profile: OpenGL ARB vertex program */
myVertexProgramName, /* Entry function name */
NULL); /* No extra compiler options */
cgGLLoadProgram(myCgVertexProgram);
//【3】将shader载入,并运行
glutDisplayFunc(render);
glutReshapeFunc(reshape);
glEnable(GL_DEPTH_TEST);
glutMainLoop();
return 0;
}
void reshape(int w, int h)
{
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45, (float)w/(float)h, 0.1, 100);
glViewport(0,0,w,h);
ww = w;
hh = h;
}
void render()
{
displayFPS();
//【3.1】gl视图变换
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glClearColor(.0f, .0f, .2f, 1.0f);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(.0,.0,5.0, .0,.0,.0, .0,1.0,.0);
static float angle;
glRotatef(angle, 0.0,1.0,0.0);
//【3.2】把程序与当前API状态绑定起来;
cgGLBindProgram(myCgVertexProgram);
cgGLEnableProfile(myCgVertexProfile);
//将ModelViewProjection矩阵传入shader
CGparameter mvp = cgGetNamedParameter(myCgVertexProgram, "MVP");
cgGLSetStateMatrixParameter(mvp, CG_GL_MODELVIEW_PROJECTION_MATRIX, CG_GL_MATRIX_IDENTITY);
CGparameter mv = cgGetNamedParameter(myCgVertexProgram, "MV");
cgGLSetStateMatrixParameter(mv, CG_GL_MODELVIEW_MATRIX, CG_GL_MATRIX_IDENTITY);
//注意传参的方法☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆
lp = cgGetNamedParameter(myCgVertexProgram, "LightPosition");
ep = cgGetNamedParameter(myCgVertexProgram, "eyePosition");
i = cgGetNamedParameter(myCgVertexProgram, "I");
a = cgGetNamedParameter(myCgVertexProgram, "Ka");
d = cgGetNamedParameter(myCgVertexProgram, "Kd");
s = cgGetNamedParameter(myCgVertexProgram, "Ks");
n = cgGetNamedParameter(myCgVertexProgram, "shininess");
cgSetParameter3f(lp,1.0,1.0,1.0);
cgSetParameter3f(ep,0,0.5,0);
cgSetParameter3f(i ,1.0, 1.0, 1.0);
cgSetParameter3f(a ,0.15,0.15, 0.0);
cgSetParameter3f(d ,1.0, 1.0 , 0.0);
cgSetParameter3f(s ,1.0, 1.0 , 1.0);
cgSetParameter1f(n ,32);
glutSolidTorus(0.3,1.0,30,30);
cgGLDisableProfile(myCgVertexProfile);
angle += 0.5;
if(angle >=360) angle = 0.0f;
glutSwapBuffers();
glutPostRedisplay();
}
由于是基于vertex的光照,虽然采用gouraud shading要比flat shading效果好的多,但是和phong shading的效果相差很大。这里大家要注意的是,phong model和phong shading的区别。上一篇教程所讲的光照模型叫phong model,而这篇教程要介绍的一种着色方法叫phong shading,必须要使用shader才能实现。下面的图中对比了采用flat shading,gouraud shading和phong shading技术渲染的一个圆环。
Flat shading |
Gouraud shading |
Phong shading |
Cg来实现基于pixel lighing的phong shading光照就容易多了。绝大部分的Cg shader代码都是一样的,主要的改变就是这次不是在vertex shader里光照,而是在fragment shader里计算光照。所以整个vertex shader的代码很简单,将要渲染的物体的顶点位置,顶点法线传入fragment shader就是vertex shader的全部工作。整个vertex shader的代码如下。
上面的代码的输出结构体中有三个成员。Position是传入的物体的顶点,该顶点将用ModelViewProjection矩阵转换成剪裁坐标系中的坐标供光栅化使用。一旦将坐标转换后,我们就无法在fragment shader中使用物体的顶点坐标了。由于要在fragmentshader中计算光照,所以我们要将物体的顶点位置,顶点法线都传入到fragment shader中。这里物体转换前的顶点和法线分别使用了语义TEXCOORD0和TEXCOORD1,代表将它们作为纹理坐标后传入fragment shader。这样GPU会把顶点和法线信息当做纹理坐标来处理,在贴图的时候,GPU会根据纹理自动插值计算每个像素对应的颜色,而现在GPU插值计算出来的就是每个像素对应的坐标和法线信息了。有了这些数据就可以进行phong shading的计算了。光照计算的公式和方法和上一个教程介绍的一模一样。
vertex脚本:
struct output { float4 position : POSITION; float3 objectPos : TEXCOORD0; float3 normal : TEXCOORD1; }; output v_main( float4 position : POSITION, float3 normal : NORMAL, uniform float4x4 MV, uniform float4x4 MVP ) { output OUT; OUT.position = mul(MVP, position); OUT.objectPos = mul(MV, position).xyz; OUT.normal = mul(MV, float4(normal,0.0)).xyz; return OUT; }
fragment脚本:
uniform float3 LightPosition; uniform float3 eyePosition; uniform float3 I; uniform float3 Ka; uniform float3 Kd; uniform float3 Ks; uniform float shininess; struct input{ float3 objectPos: TEXCOORD0; float3 normal : TEXCOORD1; }; struct output{ float4 color : COLOR; }; output f_main( in input IN ) { output OUT; float3 N = normalize(IN.normal); float3 P = IN.objectPos; float3 L = normalize(LightPosition - P); float NdotL = max(dot(N,L),0); float3 ambient = Ka * I; float3 diffuse = Kd * I * NdotL; float3 V = normalize(eyePosition - P); float3 H = normalize(L+V); float NdotH = pow(max(dot(N,H), 0), shininess); if(NdotL<=0) NdotH = 0.0; float3 specular = Ks*I*NdotH; float3 color = ambient + diffuse + specular; OUT.color.xyz= color; OUT.color.w = 1.0; return OUT; }
主程序:
#include <gl/glut.h> #include <cg/cg.h> #include <Cg/cgGL.h> #include <stdio.h> #include <Windows.h> int ww = 640, hh = 480; void render(); void reshape(int w, int h); static CGcontext myCgContext; static CGprofile myCgVertexProfile; static CGprofile myCgFragmentProfile; static CGprogram myCgVertexProgram; static CGprogram myCgFragmentProgram; static const char *myProgramName = "Lighting CG2"; static const char *myVertexProgramFileName = "CSDN_03v.cg"; static const char *myVertexProgramName = "v_main"; static const char *myFragmentProgramFileName = "CSDN_03f.cg"; static const char *myFragmentProgramName = "f_main"; CGparameter lp, ep, i, a, d, s, n; //display FPS on the title of the window void displayFPS(){ static float lastTime = 0.0f; static float framesPerSecond = 0.0f; // This will store our fps // This will hold the time from the last frame float currentTime = GetTickCount() * 0.001f; if( currentTime - lastTime > 0.0f ) { framesPerSecond = 1/(currentTime - lastTime); char strFrameRate[256]; lastTime = currentTime; sprintf(strFrameRate, "Current Frames Per Second: %f", framesPerSecond); glutSetWindowTitle( strFrameRate); framesPerSecond = 0; } } int main(int argc, char** argv) { //【1】初始化部分 glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE | GLUT_DEPTH); glutInitWindowSize(ww,hh); glutCreateWindow(myProgramName); //【2】构建shader运行环境; myCgContext = cgCreateContext(); cgGLSetDebugMode(CG_FALSE); cgSetParameterSettingMode(myCgContext, CG_DEFERRED_PARAMETER_SETTING); myCgVertexProfile = cgGLGetLatestProfile(CG_GL_VERTEX); cgGLSetOptimalOptions(myCgVertexProfile); myCgVertexProgram = cgCreateProgramFromFile( myCgContext, /* Cg runtime context */ CG_SOURCE, /* Program in human-readable form */ myVertexProgramFileName, /* Name of file containing program */ myCgVertexProfile, /* Profile: OpenGL ARB vertex program */ myVertexProgramName, /* Entry function name */ NULL); /* No extra compiler options */ cgGLLoadProgram(myCgVertexProgram); myCgFragmentProfile = cgGLGetLatestProfile(CG_GL_FRAGMENT); cgGLSetOptimalOptions(myCgFragmentProfile); myCgFragmentProgram = cgCreateProgramFromFile( myCgContext, /* Cg runtime context */ CG_SOURCE, /* Program in human-readable form */ myFragmentProgramFileName, /* Name of file containing program */ myCgFragmentProfile, /* Profile: OpenGL ARB vertex program */ myFragmentProgramName, /* Entry function name */ NULL); /* No extra compiler options */ cgGLLoadProgram(myCgFragmentProgram); //【3】将shader载入,并运行 glutDisplayFunc(render); glutReshapeFunc(reshape); glEnable(GL_DEPTH_TEST); glutMainLoop(); return 0; } void reshape(int w, int h) { glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(45, (float)w/(float)h, 0.1, 100); glViewport(0,0,w,h); ww = w; hh = h; } void render() { displayFPS(); //【3.1】gl视图变换 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClearColor(.0f, .0f, .2f, 1.0f); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); gluLookAt(.0,.0,5.0, .0,.0,.0, .0,1.0,.0); static float angle; glRotatef(angle, 0.0,1.0,0.0); //【3.2】把程序与当前API状态绑定起来; cgGLBindProgram(myCgVertexProgram); cgGLEnableProfile(myCgVertexProfile); cgGLBindProgram(myCgFragmentProgram); cgGLEnableProfile(myCgFragmentProfile); //将ModelViewProjection矩阵传入shader CGparameter mvp = cgGetNamedParameter(myCgVertexProgram, "MVP"); cgGLSetStateMatrixParameter(mvp, CG_GL_MODELVIEW_PROJECTION_MATRIX, CG_GL_MATRIX_IDENTITY); CGparameter mv = cgGetNamedParameter(myCgVertexProgram, "MV"); cgGLSetStateMatrixParameter(mv, CG_GL_MODELVIEW_MATRIX, CG_GL_MATRIX_IDENTITY); //传参的方法 lp = cgGetNamedParameter(myCgFragmentProgram, "LightPosition"); ep = cgGetNamedParameter(myCgFragmentProgram, "eyePosition"); i = cgGetNamedParameter(myCgFragmentProgram, "I"); a = cgGetNamedParameter(myCgFragmentProgram, "Ka"); d = cgGetNamedParameter(myCgFragmentProgram, "Kd"); s = cgGetNamedParameter(myCgFragmentProgram, "Ks"); n = cgGetNamedParameter(myCgFragmentProgram, "shininess"); cgSetParameter3f(lp,1.0,1.0,1.0); cgSetParameter3f(ep,0,0.5,0); cgSetParameter3f(i ,1.0, 1.0, 1.0); cgSetParameter3f(a ,0.15,0.15, 0.0); cgSetParameter3f(d ,1.0, 1.0 , 0.0); cgSetParameter3f(s ,1.0, 1.0 , 1.0); cgSetParameter1f(n ,32); glutSolidTorus(0.3,1.0,30,30); cgGLDisableProfile(myCgVertexProfile); cgGLDisableProfile(myCgFragmentProfile); angle += 0.5; if(angle >=360) angle = 0.0f; glutSwapBuffers(); glutPostRedisplay(); }
phong model结果1: phong shading结果2: