Phong光照很棒,而且性能较高,但是它的镜面反射在某些条件下会失效,特别是当发光值属性低的时候,对应一个非常大的粗糙的镜面区域。下面的图片展示了,当我们使用镜面的发光值为1.0时,一个带纹理地板的效果:
你可以看到,镜面区域边缘迅速减弱并截止。出现这个问题的原因是在视线向量和反射向量的角度不允许大于90度。如果大于90度的话,点乘的结果就会是负数,镜面的贡献成分就会变成0。你可能会想,这不是一个问题,因为大于90度时我们不应看到任何光,对吧?
错了,这只适用于漫散射部分,当法线和光源之间的角度大于90度时意味着光源在被照亮表面的下方,这样光的散射成分就会是0.0。然而,对于镜面光照,我们不会测量光源和法线之间的角度,而是测量视线和反射方向向量之间的。看看下面的两幅图:
现在看来问题就很明显了。左侧图片显示Phong反射的θ小于90度的情况。我们可以看到右侧图片视线和反射之间的角θ大于90度,这样镜面反射成分将会被消除。通常这也不是问题,因为视线方向距离反射方向很远,但如果我们使用一个数值较低的发光值参数的话,镜面半径就会足够大,以至于能够贡献一些镜面反射的成份了。在例子中,我们在角度大于90度时消除了这个贡献(如第一个图片所示)。
1977年James F. Blinn引入了Blinn-Phong着色,它扩展了我们目前所使用的Phong着色。Blinn-Phong模型很大程度上和Phong是相似的,不过它稍微改进了Phong模型,使之能够克服我们所讨论到的问题。它放弃使用反射向量,而是基于我们现在所说的一个叫做半程向量(halfway vector)的向量,这是个单位向量,它在视线方向和光线方向的中间。半程向量和表面法线向量越接近,镜面反射成份就越大。
当视线方向恰好与反射向量对称时,半程向量就与法线向量重合。这样观察者距离原来的反射方向越近,镜面反射的高光就会越强。
这里,你可以看到无论观察者往哪里看,半程向量和表面法线之间的夹角永远都不会超过90度(当然除了光源远远低于表面的情况)。这样会产生和Phong反射稍稍不同的结果,但这时看起来会更加可信,特别是发光值参数比较低的时候。Blinn-Phong着色模型也正是早期OpenGL固定函数输送管道(fixed function pipeline)所使用的着色模型。
得到半程向量很容易,我们将光的方向向量和视线向量相加,然后将结果归一化(normalize);
翻译成GLSL代码如下:
vec3 lightDir = normalize(lightPos - FragPos);
vec3 viewDir = normalize(viewPos - FragPos);
vec3 halfwayDir = normalize(lightDir + viewDir);
实际的镜面反射的计算,就成为计算表面法线和半程向量的点乘,并对其结果进行约束(大于或等于0),然后获取它们之间角度的余弦,再添加上发光值参数:
float spec = pow(max(dot(normal, halfwayDir), 0.0), shininess);
vec3 specular = lightColor * spec;
Blinn-Phong和Phong的镜面反射唯一不同之处在于,现在我们要测量法线和半程向量之间的角度,而半程向量是视线方向和反射向量之间的夹角。
引入了半程向量来计算镜面反射后,我们再也不会遇到Phong着色的骤然截止问题了。下图展示了两种不同方式下发光值指数为0.5时镜面区域的不同效果:
Phong和Blinn-Phong着色之间另一个细微差别是,半程向量和表面法线之间的角度经常会比视线和反射向量之间的夹角更小。结果就是,为了获得和Phong着色相似的效果,必须把发光值参数设置的大一点。通常的经验是将其设置为Phong着色的发光值参数的2至4倍。
下图是Phong指数为8.0和Blinn-Phong指数为32的时候,两种specular反射模型的对比:
你可以看到Blinn-Phong的镜面反射成分要比Phong锐利一些。这通常需要使用一点小技巧才能获得之前你所看到的Phong着色的效果,但Blinn-Phong着色的效果比默认的Phong着色通常更加真实一些。
blinnphong.vsh:
attribute vec3 position;
attribute vec3 normal;
attribute vec2 texCoords;
varying vec3 FragPos;
varying vec3 Normal;
varying vec2 TexCoords;
uniform mat4 projection;
uniform mat4 view;
void main()
{
gl_Position = projection * view * vec4(position, 1.0);
FragPos = position;
Normal = normal;
TexCoords = texCoords;
}
blinnphong.fsh:
varying vec3 FragPos;
varying vec3 Normal;
varying vec2 TexCoords;
uniform sampler2D floorTexture;
uniform vec3 lightPos;
uniform vec3 viewPos;
uniform bool blinn;
void main()
{
vec3 color = texture2D(floorTexture, TexCoords).rgb;
// Ambient, 环境光
vec3 ambient = 0.05 * color;
// Diffuse,漫反射
vec3 lightDir = normalize(lightPos - FragPos);
vec3 normal = normalize(Normal);
float diff = max(dot(lightDir, normal), 0.0);
vec3 diffuse = diff * color;
// Specular,镜面反射
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, normal);
float spec = 0.0;
if(blinn)
{
vec3 halfwayDir = normalize(lightDir + viewDir); //半程向量
spec = pow(max(dot(normal, halfwayDir), 0.0), 32.0);
}
else
{
vec3 reflectDir = reflect(-lightDir, normal);
spec = pow(max(dot(viewDir, reflectDir), 0.0), 8.0);
}
vec3 specular = vec3(0.3) * spec; // assuming bright white light color
gl_FragColor = vec4(ambient + diffuse + specular, 1.0);
// gl_FragColor = texture2D(floorTexture, TexCoords);
}
全部代码:
//
// OpenGLBlinnPhong.cpp
// shaderTest
//
// Created by MacSBL on 2017/1/5.
//
//
#include "OpenGLBlinnPhong.h"
#include "ResourceManager.h"
bool OpenGLBlinnPhong::init()
{
if (!Layer::init()) {
return false;
}
origin = Director::getInstance()->getVisibleOrigin();
vsize = Director::getInstance()->getVisibleSize();
GLfloat planeVertices[] = {
// Positions // Normals // Texture Coords
8.0f, -0.5f, 8.0f, 0.0f, 1.0f, 0.0f, 5.0f, 0.0f,
-8.0f, -0.5f, 8.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
-8.0f, -0.5f, -8.0f, 0.0f, 1.0f, 0.0f, 0.0f, 5.0f,
8.0f, -0.5f, 8.0f, 0.0f, 1.0f, 0.0f, 5.0f, 0.0f,
-8.0f, -0.5f, -8.0f, 0.0f, 1.0f, 0.0f, 0.0f, 5.0f,
8.0f, -0.5f, -8.0f, 0.0f, 1.0f, 0.0f, 5.0f, 5.0f
};
//——————————————————————————————————————————————————————————————————————————————————————————————
//设置 glprogram
auto program = new GLProgram();
program->initWithFilenames("blinnphong.vsh", "blinnphong.fsh");
program->link();
this->setGLProgram(program);
//——————————————————————————————————————————————————————————————————————————————————————————————
//地面的vao
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
GLuint vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(planeVertices), planeVertices, GL_STATIC_DRAW);
//把顶点数据传给 object.vsh vertex shader 的第一个属性(索引为0)
GLuint posloc = program->getAttribLocation("position");
glVertexAttribPointer(posloc, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(posloc);
//把法向量传给 object.vsh 的a_normal属性,注意:这里不能想当然的认为vertex shader中第2个attribute就是索引为1,必须通过program实事求是的取出它的location
GLuint normloc = glGetAttribLocation(program->getProgram(), "normal");
glEnableVertexAttribArray(normloc);
glVertexAttribPointer(normloc, 3, GL_FLOAT, GL_FALSE, 8*sizeof(GLfloat), (GLvoid*)(3*sizeof(GLfloat)));
GLuint texloc = program->getAttribLocation("texCoords");
glVertexAttribPointer(texloc, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));
glEnableVertexAttribArray(texloc);
glBindVertexArray(0);
//————————————————————————————————————————————————————————————
//地面的纹理
// auto img = Sprite::create("wall.jpg");
// floorTexture = img->getTexture()->getName();
floorTexture = ResourceManager::getInstance()->loadTexture("wall.jpg");
blinn = false;
//——————————————————————————————————————————————————————————————————————————————————————————————
//设置转换矩阵
model = new Mat4();
// model->rotate(Vec3(1, 1, 0), CC_DEGREES_TO_RADIANS(60));
// view = new Mat4();
// view->translate(0, 0, -3);
//camera
cam = new MyCamera(Vec3(0, 0, 3));
projection = new Mat4();
Mat4::createPerspective(cam->Zoom, vsize.width / vsize.height, 0.1, 1000, projection);
lightpos = Vec3(0, 0, 0);
Director::getInstance()->setDepthTest(true);
//touch事件
auto elistener = EventListenerTouchOneByOne::create();
elistener->onTouchBegan = CC_CALLBACK_2(OpenGLBlinnPhong::onTouchBegan, this);
elistener->onTouchMoved = CC_CALLBACK_2(OpenGLBlinnPhong::onTouchMoved, this);
elistener->onTouchEnded = CC_CALLBACK_2(OpenGLBlinnPhong::onTouchEnded, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(elistener, this);
return true;
}
void OpenGLBlinnPhong::visit(cocos2d::Renderer *render, const cocos2d::Mat4 &parentTransform, uint32_t parentflag)
{
Layer::visit(render, parentTransform, parentflag);
_command.init(_globalZOrder);
_command.func = CC_CALLBACK_0(OpenGLBlinnPhong::onDraw, this);
Director::getInstance()->getRenderer()->addCommand(&_command);
}
void OpenGLBlinnPhong::onDraw()
{
auto program = this->getGLProgram();
program->use();
//物体的 model、view、project 矩阵
model->scale(1);
view = cam->GetViewMatrix();
Mat4::createPerspective(cam->Zoom, vsize.width / vsize.height, 0.1, 100, projection);
// GLuint modeloc = glGetUniformLocation(program->getProgram(), "model");
// glUniformMatrix4fv(modeloc, 1, GL_FALSE, model->m);
GLuint viewloc = glGetUniformLocation(program->getProgram(), "view");
glUniformMatrix4fv(viewloc, 1, GL_FALSE, view->m);
GLuint proloc = glGetUniformLocation(program->getProgram(), "projection");
glUniformMatrix4fv(proloc, 1, GL_FALSE, projection->m);
//set light uniforms
GLuint lightposLoc = glGetUniformLocation(program->getProgram(), "lightpos");
glUniform3f(lightposLoc, lightpos.x, lightpos.y, lightpos.z);
GLuint viewposloc = glGetUniformLocation(program->getProgram(), "viewPos");
glUniform3f(viewposloc, cam->Position.x, cam->Position.y, cam->Position.z);
GLuint blinnloc = program->getUniformLocation("blinn");
glUniform1i(blinnloc, blinn);
glBindVertexArray(vao);
GL::bindTexture2DN(0, floorTexture);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
}
#pragma mark touch
bool OpenGLBlinnPhong::onTouchBegan(cocos2d::Touch *touch, cocos2d::Event *evt)
{
return true;
}
void OpenGLBlinnPhong::onTouchMoved(cocos2d::Touch *touch, cocos2d::Event *evt)
{
Vec2 curpos = touch->getLocationInView();
Vec2 prepos = touch->getPreviousLocationInView();
GLfloat dx = curpos.x - prepos.x;
GLfloat dy = curpos.y - prepos.y;
//移动摄像机
GLfloat camspeed = 0.05f;
if (curpos.y - prepos.y > 0) { //w
cam->ProcessKeyboard(Cam_Move::FORWARD, camspeed);
}else if (curpos.y - prepos.y < 0){ //s
cam->ProcessKeyboard(Cam_Move::BACKWARD, camspeed);
}
if (curpos.x - prepos.x < 0){ //a
cam->ProcessKeyboard(Cam_Move::LEFT, camspeed);
}else if (curpos.x - prepos.x > 0){ //d
cam->ProcessKeyboard(Cam_Move::RIGHT, camspeed);
}
//(3)旋转摄像机
// cam->ProcessMouseMovement(dx, dy);
//(4)缩放
// if(fov >= 1 && fov <= 45){
// fov -= dx * camspeed;
// }
// if(fov <= 1){
// fov = 1;
// }
// if(fov >= 45){
// fov = 45;
// }
}
void OpenGLBlinnPhong::onTouchEnded(cocos2d::Touch *touch, cocos2d::Event *evt)
{
}
ResourceManager:
//
// ResourceManager.cpp
// shaderTest
//
// Created by MacSBL on 2017/3/9.
//
//
#include "ResourceManager.h"
static ResourceManager* _instance;
ResourceManager* ResourceManager::getInstance()
{
if (_instance == nullptr) {
_instance = new ResourceManager();
}
return _instance;
}
ResourceManager::ResourceManager()
{
}
ResourceManager::~ResourceManager()
{
}
GLuint ResourceManager::loadTexture(const std::string& path)
{
// Generate texture ID and load texture data
GLuint textureID;
glGenTextures(1, &textureID);
auto img = new cocos2d::Image();
img->initWithImageFile(path);
int width = img->getWidth();
int height = img->getHeight();
unsigned char* imgdata = img->getData();
// Assign texture to ID
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, imgdata);
glGenerateMipmap(GL_TEXTURE_2D);
// Parameters
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
return textureID;
}