首页 > 代码库 > OpenGL进阶(十九) - 多光源
OpenGL进阶(十九) - 多光源
从光说起
先看一段wiki中对光的定义
光是一种人类眼睛可以见的电磁波(可见光谱),视知觉就是对于光的知觉[1]。光只是电磁波谱上的某一段频谱,一般是定义为波长介于400至700纳米(nm)之间的电磁波,也就是波长比紫外线长,比红外线短的电磁波。有些资料来源定义的可见光的波长范围也有不同,较窄的有介于420至680纳米,较宽的有介于380至800纳米。
光既是一种高频的电磁波,又是一种由称为光子的基本粒子组成的粒子流。因此光同时具有粒子性与波动性,或者说光具有“波粒二象性”。
按波动理论来解释,不同颜色的光就是不同波长的电磁波。
光的衰减(Attenuation)
任何点光源的光照亮度随着距离增加会迅速衰减。这个就叫光的衰减。
不同波长的光有不同的衰减方式,还有就是灯光所处的环境,比如是否有雾霾,下面是常见的几种衰减模型。
这里实现一个衰减的类,作为灯光的成员
#pragma once class Attenuation { public: Attenuation(float range, float constant, float linear, float quadratic) : m_range(range), m_constant(constant), m_linear(linear), m_quadratic(quadratic) {} inline float getRange() const { return m_range; } inline float getConstant() const { return m_constant; } inline float getLinear() const { return m_linear; } inline float getQuadratic() const { return m_quadratic; } private: float m_constant; float m_linear; float m_quadratic; float m_range; };
Attenuation类有4个成员后面三项是常量项,线性项还有二次项,最后的衰减率的计算是由下面的公式确定的,Distance表示光源到点的距离。
attenuation = Constant + Linear * Distance + Quadratic * Distance ^ 2
第一个成员表示光源照亮的范围,下面有一个表可以用来查询四者之间的关系
当你选定了一个Rang的时候,你就可以找到对应的constant,linear 和 quadratic。
Constant 越趋近于0,灯光就越亮,反之越暗。
Linear越大,灯光衰减得就越快。不建议改变Quadratic值或者减少Linear的值,这样做需要重新计算Range.
减少Range值可以提升渲染的速度,但是减少得太多,在游戏中可能会造成灯光效果的突变。
点光源
首先创建一个BaseLight类,作为各种灯光的基类
#pragma once #include "common.h" class BaseLight { public: BaseLight(const glm::vec3& color, const glm::vec3& pos, float intensity) : m_color(color), m_pos(pos), m_intensity(intensity) {} inline glm::vec3 getPos() const {return m_pos;} inline float getIntensity() { return m_intensity; } inline glm::vec3 getColor() { return m_color; } private: glm::vec3 m_color; glm::vec3 m_pos; float m_intensity; };
注意,很多教程上光的属性有ambient,diffuse,specular之类,按照前面的原理,这都是不科学的,包括材质的Ambient,其实环境光应该是一个全局常量,所以材质也只能diffuse, specular. 插一段StackOverflow上的回答。
这里基础 灯光只有三个成员,颜色,位置,强度。
点光源的类
#pragma once #include "baselight.h" #include "attenuation.h" class PointLight :public BaseLight { public: public: PointLight(const glm::vec3& color = glm::vec3(0, 0, 0), const glm::vec3& pos = glm::vec3(0, 0, 0), const float intensity = 1.0, const Attenuation& atten = Attenuation()): BaseLight(color,pos,intensity),m_attenuation(atten){} inline const Attenuation& getAttenuation() const { return m_attenuation; } private: Attenuation m_attenuation; };
灯光的初始化,So easy.
pointLight = new PointLight(glm::vec3(0, 1, 0), glm::vec3(3, 3, 3), 1.8, Attenuation(20, 1.0, 0.22, 0.20));
给shader传参数
prog.setUniform("pointLight.pos", pointLight->getPos()); prog.setUniform("pointLight.color", pointLight->getColor()); prog.setUniform("pointLight.intensity", pointLight->getIntensity()); prog.setUniform("pointLight.constant", pointLight->getAttenuation().getConstant()); prog.setUniform("pointLight.linear", pointLight->getAttenuation().getLinear()); prog.setUniform("pointLight.quadratic", pointLight->getAttenuation().getQuadratic()); prog.setUniform("pointLight.range", pointLight->getAttenuation().getRange());
接下来就是shader了
vertex shader 就是传个值。
#version 400 layout (location = 0) in vec3 VertexPosition; layout (location = 1) in vec2 VertexUv; layout (location = 2) in vec3 VertexNormal; uniform mat4 MVP; out vec3 position; out vec3 normal; void main() { normal = VertexNormal; position = VertexPosition; gl_Position = MVP * vec4( VertexPosition, 1.0); }
fragment shader也就是三板斧,ambient,diffuse,specular,具体计算看Code
#version 400 struct PointLight { float range; vec3 pos; vec3 color; float intensity; float constant; float linear; float quadratic; }; struct MaterialInfo{ vec3 diffuse; vec3 specular; float shininess; }; uniform vec3 ambient; uniform PointLight pointLight; uniform MaterialInfo materialInfo; uniform vec3 cameraPosition; in vec3 position; in vec3 normal; out vec4 finalColor; vec3 calculatePointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir) { vec3 lightDir = normalize(light.pos - fragPos); //ambient vec3 ambFactor = ambient; // Diffuse shading float diffFactor = max(dot(normal, lightDir), 0.0) * light.intensity ; // Specular shading vec3 reflectDir = normalize(reflect(-lightDir, normal)); float specFactor = pow(max(dot(viewDir, reflectDir), 0.0), materialInfo.shininess) * light.intensity; // Attenuation float distance = length(light.pos - fragPos); float attenuation = 1.0f; if(distance < light.range) { attenuation = 1.0f / (light.constant + light.linear * distance + light.quadratic * (distance * distance)); } vec3 ambientColor = ambFactor; vec3 diffuseColor = diffFactor * materialInfo.diffuse * light.color; vec3 specularColor = specFactor * materialInfo.specular * light.color ; return ambientColor + attenuation * (diffuseColor + specularColor); } void main(void) { vec3 totalLight = vec3(0,0,0); vec3 norm = normalize(normal); vec3 viewDir = normalize(cameraPosition - position); totalLight += calculatePointLight(pointLight, normal, position, viewDir); finalColor = vec4(totalLight, 1.0); return; }
运行结果
多光源
先看下最终的效果,是不是有点炫酷!?
整体的思路是:创建3个点光源,然后传到把点光源的信息都传递进去,开一个定时器,不断更新光源的位置,再更新shader数据,最后再绘制。
首先创建一个简单的场景类,注意这个类是要继承QObject的,因为要用到Qt的Signal/Slot机制。
#ifndef SCENE_H #define SCENE_H #include <vector> #include <QDebug> #include <QTimer> #include <QObject> #include "light/pointlight.h" #include "shader/shaderprogram.h" #include <QObject> class Scene : public QObject { Q_OBJECT public: Scene(QObject *parent = 0); ~Scene(); void addLight(PointLight* pLight); void setShader(ShaderProgram *pShader); void setUniform(); private: std::vector<PointLight * > pointLights; ShaderProgram *shaderProgram; QTimer *updateTimer; private slots: void updateLight(); signals: void updateScene(); }; #endif // SCENE_H
接下来是cpp的实现
#include "scene.h" Scene::Scene(QObject *parent) : QObject(parent) { updateTimer = new QTimer(); updateTimer->setInterval(30); connect(updateTimer, SIGNAL(timeout()), this, SLOT(updateLight())); updateTimer->start(); } Scene::~Scene() { for (int i = 0; i < pointLights.size(); i++) { delete pointLights[i]; } } void Scene::addLight(PointLight* pLight) { pointLights.push_back(pLight); } void Scene::setUniform() { char tmp[100]; int count = static_cast<int>(pointLights.size()); shaderProgram->setUniform("pointLightCount", count); for (int i = 0; i < count; i++) { sprintf(tmp, "pointLights[%d].pos", i); shaderProgram->setUniform(tmp, pointLights[i]->getPos()); sprintf(tmp, "pointLights[%d].color", i); shaderProgram->setUniform(tmp, pointLights[i]->getColor()); sprintf(tmp, "pointLights[%d].intensity", i); shaderProgram->setUniform(tmp, pointLights[i]->getIntensity()); sprintf(tmp, "pointLights[%d].constant", i); shaderProgram->setUniform(tmp, pointLights[i]->getAttenuation().getConstant()); sprintf(tmp, "pointLights[%d].linear", i); shaderProgram->setUniform(tmp, pointLights[i]->getAttenuation().getLinear()); sprintf(tmp, "pointLights[%d].quadratic", i); shaderProgram->setUniform(tmp, pointLights[i]->getAttenuation().getQuadratic()); sprintf(tmp, "pointLights[%d].range", i); shaderProgram->setUniform(tmp, pointLights[i]->getAttenuation().getRange()); } } void Scene::setShader(ShaderProgram *pShader) { shaderProgram = pShader; } void Scene::updateLight() { glm::mat4 transMatrix = glm::translate(glm::mat4(1.0f), glm::vec3(0, 2, 0)); glm::mat4 rotationX = glm::rotate(transMatrix, 0.1f, glm::vec3(1, 0, 0)); glm::mat4 rotationY = glm::rotate(transMatrix, 0.1f, glm::vec3(0, 1, 0)); glm::mat4 rotationZ = glm::rotate(transMatrix, 0.1f, glm::vec3(0, 0, 1)); glm::vec4 newPos = glm::vec4(pointLights[0]->getPos(), 1.0f) * rotationX; pointLights[0]->setPos(glm::vec3(newPos)); newPos = glm::vec4(pointLights[1]->getPos(), 1.0f) * rotationY; pointLights[1]->setPos(glm::vec3(newPos)); newPos = glm::vec4(pointLights[2]->getPos(), 1.0f) * rotationZ; pointLights[2]->setPos(glm::vec3(newPos)); this->setUniform(); emit updateScene(); }
解释两个函数,
setUniform
向shader中传递当前灯光的参数。
updateLight
更新灯光的位置,不知道咋转的回去看线性代数。想偷懒的看这个 Real-Time Rendering (2) - 变换和矩阵(Transforms and Matrics)。
更新完之后调用setUniform传递参数。
最后看他们的初始化
void MainWidget::initScene() { // Calculate aspect ratio float aspect = float(width()) / float(height() ? height() : 1); const float zNear = 0.01, zFar = 100.0, fov = 45.0; // Set projection mainCamera = new Camera(glm::vec3(0, 5, 10), glm::vec3(0, 3, 0), glm::vec3(0.0, 1.0, 0.0)); mainCamera->setPerspectiveParameter(fov, aspect, zNear, zFar); modelMatrix = glm::mat4(1.0f); scene = new Scene(); connect(scene, SIGNAL(updateScene()), this, SLOT(update())); scene->setShader(&prog); PointLight *pointLight1 = new PointLight(glm::vec3(0, 1, 0), glm::vec3(0, 2, 3), 1.8, Attenuation(20, 0.1, 0.22, 0.20)); PointLight *pointLight2 = new PointLight(glm::vec3(1, 0, 0), glm::vec3(3, 2, 0), 1.8, Attenuation(20, 0.1, 0.22, 0.20)); PointLight *pointLight3 = new PointLight(glm::vec3(0, 0, 1), glm::vec3(-3, 2, 0), 1.8, Attenuation(20, 0.1, 0.22, 0.20)); scene->addLight(pointLight1); scene->addLight(pointLight2); scene->addLight(pointLight3); compileShader(); setUniform(); objModel.loadFromFile("../Assets/model/bunny.obj"); objModel.setShader(prog); }
Fragment Shader需要做一些改变
#version 400 const int MAX_POINT_LIGHTS = 5; struct PointLight { float range; vec3 pos; vec3 color; float intensity; float constant; float linear; float quadratic; }; struct MaterialInfo{ vec3 diffuse; vec3 specular; float shininess; }; uniform vec3 ambient; uniform int pointLightCount; uniform PointLight pointLights[MAX_POINT_LIGHTS]; uniform MaterialInfo materialInfo; uniform vec3 cameraPosition; in vec3 position; in vec3 normal; out vec4 finalColor; vec3 calculatePointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir) { vec3 lightDir = normalize(light.pos - fragPos); //ambient vec3 ambFactor = ambient; // Diffuse shading float diffFactor = max(dot(normal, lightDir), 0.0) * light.intensity ; // Specular shading vec3 reflectDir = normalize(reflect(-lightDir, normal)); float specFactor = pow(max(dot(viewDir, reflectDir), 0.0), materialInfo.shininess) * light.intensity; // Attenuation float distance = length(light.pos - fragPos); float attenuation = 1.0f; if(distance < light.range) { attenuation = 1.0f / (light.constant + light.linear * distance + light.quadratic * (distance * distance)); } vec3 ambientColor = ambFactor; vec3 diffuseColor = diffFactor * materialInfo.diffuse * light.color; vec3 specularColor = specFactor * materialInfo.specular * light.color ; //return ambientColor + attenuation * (diffuseColor + specularColor); return attenuation * (diffuseColor + specularColor); } void main(void) { vec3 totalLight = vec3(0,0,0); vec3 norm = normalize(normal); vec3 viewDir = normalize(cameraPosition - position); for(int i = 0; i < pointLightCount; i++) { totalLight += calculatePointLight(pointLights[i], normal, position, viewDir); } finalColor = vec4(totalLight, 1.0); return; }
打完收工。
参考
Point Light Attenuation - http://www.ogre3d.org/tikiwiki/tiki-index.php?page=-Point+Light+Attenuation
Multiple lights - http://www.learnopengl.com/#!Lighting/Multiple-lights
Modern OpenGL 07 – More Lighting: Ambient, Specular, Attenuation, Gamma - http://www.tomdalling.com/blog/modern-opengl/07-more-lighting-ambient-specular-attenuation-gamma/
BennyQBD/3DEngineCpp - https://github.com/BennyQBD/3DEngineCpp
OpenGL进阶(十九) - 多光源