從零開始手敲次世代遊戲引擎(五十四)
來自專欄 高品質遊戲開發
在從零開始手敲次世代遊戲引擎(五十三)當中,我們實現了一個較為複雜的單一光源(射燈)。本篇我們來實現多種光源的混合照射。
為了支持多種光源的混合照射,我們需要分別修改片元著色器和RHI當中的代碼,使其支持多光源參數的傳遞。以OpenGL為例,我們首先可以修改GLSL的代碼,定義一個存儲光源常量的數組:
/////////////////////// per frame#define MAX_LIGHTS 10uniform int numLights;uniform struct Light { vec4 lightPosition; vec4 lightColor; vec3 lightDirection; float lightIntensity; int lightDistAttenCurveType; float lightDistAttenCurveParams[5]; int lightAngleAttenCurveType; float lightAngleAttenCurveParams[5];} allLights[MAX_LIGHTS];
雖然在OpenGL 4.3版本之後,我們可以定義不定長的數組,但是我們這裡為了更好的支持移動設備,採用了固定長度的數組。我們這裡假定最多支持10個光源。
接下來我們將片元著色器(fragment shader)當中光照計算的部分獨立到一個單獨的函數當中,在main當中循環所有的光源,為每個光源調用這個光照計算函數一次,並將結果進行累加,得到最終的光照結果:
vec3 apply_light(Light light) { vec3 N = normalize(normal.xyz); vec3 L = (viewMatrix * worldMatrix * light.lightPosition).xyz - v.xyz; float lightToSurfDist = length(L); L = normalize(L); vec3 light_dir = normalize((viewMatrix * worldMatrix * vec4(light.lightDirection, 0.0f)).xyz); float lightToSurfAngle = acos(dot(L, light_dir)); // angle attenuation float atten = apply_atten_curve(lightToSurfAngle, light.lightAngleAttenCurveType, light.lightAngleAttenCurveParams); // distance attenuation atten *= apply_atten_curve(lightToSurfDist, light.lightDistAttenCurveType, light.lightDistAttenCurveParams); vec3 R = normalize(2.0f * clamp(dot(L, N), 0.0f, 1.0f) * N - L); vec3 V = normalize(v.xyz); vec3 linearColor = vec3(0); if (usingDiffuseMap) linearColor = ambientColor.rgb + light.lightIntensity * atten * light.lightColor.rgb * (texture(diffuseMap, uv).rgb * clamp(dot(N, L), 0.0f, 1.0f) + specularColor.rgb * pow(clamp(dot(R, V), 0.0f, 1.0f), specularPower)); else linearColor = ambientColor.rgb + light.lightIntensity * atten * light.lightColor.rgb * (diffuseColor.rgb * clamp(dot(N, L), 0.0f, 1.0f) + specularColor.rgb * pow(clamp(dot(R,V), 0.0f, 1.0f), specularPower)); return linearColor;}void main(void){ vec3 linearColor = vec3(0); for (int i = 0; i < numLights; i++) { linearColor += apply_light(allLights[i]); } outputColor = vec4(clamp(linearColor, 0.0f, 1.0f), 1.0f);}
接下來是修改C++的部分。我們首先需要相應調整我們的DrawFrameContext構造體的定義,使其可以容納多個光源:
struct Light{ Vector4f m_lightPosition; Vector4f m_lightColor; Vector3f m_lightDirection; float m_lightIntensity; AttenCurveType m_lightDistAttenCurveType; float m_lightDistAttenCurveParams[5]; AttenCurveType m_lightAngleAttenCurveType; float m_lightAngleAttenCurveParams[5]; }; struct DrawFrameContext { Matrix4X4f m_worldMatrix; Matrix4X4f m_viewMatrix; Matrix4X4f m_projectionMatrix; Vector3f m_ambientColor; vector<Light> m_lights; };
接下來我們在每幀計算光照的地方,從場景獲取所有光源並壓入這個結構當中:
void GraphicsManager::CalculateLights(){ m_DrawFrameContext.m_ambientColor = { 0.01f, 0.01f, 0.01f }; m_DrawFrameContext.m_lights.clear(); auto& scene = g_pSceneManager->GetSceneForRendering(); for (auto LightNode : scene.LightNodes) { Light light; auto pLightNode = LightNode.second.lock(); if (!pLightNode) continue; auto trans_ptr = pLightNode->GetCalculatedTransform(); light.m_lightPosition = { 0.0f, 0.0f, 0.0f, 1.0f }; Transform(light.m_lightPosition, *trans_ptr); light.m_lightDirection = { 0.0f, 0.0f, -1.0f }; TransformCoord(light.m_lightDirection, *trans_ptr); auto pLight = scene.GetLight(pLightNode->GetSceneObjectRef()); if (pLight) { light.m_lightColor = pLight->GetColor().Value; light.m_lightIntensity = pLight->GetIntensity(); const AttenCurve& atten_curve = pLight->GetDistanceAttenuation(); light.m_lightDistAttenCurveType = atten_curve.type; memcpy(light.m_lightDistAttenCurveParams, &atten_curve.u, sizeof(atten_curve.u)); if (pLight->GetType() == SceneObjectType::kSceneObjectTypeLightSpot) { auto plight = dynamic_pointer_cast<SceneObjectSpotLight>(pLight); const AttenCurve& angle_atten_curve = plight->GetAngleAttenuation(); light.m_lightAngleAttenCurveType = angle_atten_curve.type; memcpy(light.m_lightAngleAttenCurveParams, &angle_atten_curve.u, sizeof(angle_atten_curve.u)); } } else { assert(0); } m_DrawFrameContext.m_lights.push_back(light); }}
最後我們則需要在RHI層,設置Shader Constants的地方,將所有光源的光照參數傳遞給Shader:
bool OpenGLGraphicsManager::SetPerFrameShaderParameters(GLuint shader){ unsigned int location; // Set the world matrix in the vertex shader. location = glGetUniformLocation(shader, "worldMatrix"); if(location == -1) { return false; } glUniformMatrix4fv(location, 1, false, m_DrawFrameContext.m_worldMatrix); // Set the view matrix in the vertex shader. location = glGetUniformLocation(shader, "viewMatrix"); if(location == -1) { return false; } glUniformMatrix4fv(location, 1, false, m_DrawFrameContext.m_viewMatrix); // Set the projection matrix in the vertex shader. location = glGetUniformLocation(shader, "projectionMatrix"); if(location == -1) { return false; } glUniformMatrix4fv(location, 1, false, m_DrawFrameContext.m_projectionMatrix); location = glGetUniformLocation(shader, "ambientColor"); if(location == -1) { return false; } glUniform4fv(location, 1, m_DrawFrameContext.m_ambientColor); location = glGetUniformLocation(shader, "numLights"); if(location == -1) { return false; } glUniform1i(location, m_DrawFrameContext.m_lights.size()); // Set lighting parameters for PS shader for (size_t i = 0; i < m_DrawFrameContext.m_lights.size(); i++) { ostringstream ss; string uniformName; ss << "allLights[" << i << "]." << "lightPosition" << ends; uniformName = ss.str(); location = glGetUniformLocation(shader, uniformName.c_str()); if(location == -1) { return false; } glUniform4fv(location, 1, m_DrawFrameContext.m_lights[i].m_lightPosition); ss.clear(); ss.seekp(0); ss << "allLights[" << i << "]." << "lightColor" << ends; uniformName = ss.str(); location = glGetUniformLocation(shader, uniformName.c_str()); if(location == -1) { return false; } glUniform4fv(location, 1, m_DrawFrameContext.m_lights[i].m_lightColor); ss.clear(); ss.seekp(0); ss << "allLights[" << i << "]." << "lightIntensity" << ends; uniformName = ss.str(); location = glGetUniformLocation(shader, uniformName.c_str()); if(location == -1) { return false; } glUniform1f(location, m_DrawFrameContext.m_lights[i].m_lightIntensity); ss.clear(); ss.seekp(0); ss << "allLights[" << i << "]." << "lightDirection" << ends; uniformName = ss.str(); location = glGetUniformLocation(shader, uniformName.c_str()); if(location == -1) { return false; } glUniform3fv(location, 1, m_DrawFrameContext.m_lights[i].m_lightDirection); ss.clear(); ss.seekp(0); ss << "allLights[" << i << "]." << "lightDistAttenCurveType" << ends; uniformName = ss.str(); location = glGetUniformLocation(shader, uniformName.c_str()); if(location == -1) { return false; } glUniform1i(location, (GLint)m_DrawFrameContext.m_lights[i].m_lightDistAttenCurveType); ss.clear(); ss.seekp(0); ss << "allLights[" << i << "]." << "lightDistAttenCurveParams" << ends; uniformName = ss.str(); location = glGetUniformLocation(shader, uniformName.c_str()); if(location == -1) { return false; } glUniform1fv(location, 5, m_DrawFrameContext.m_lights[i].m_lightDistAttenCurveParams); ss.clear(); ss.seekp(0); ss << "allLights[" << i << "]." << "lightAngleAttenCurveType" << ends; uniformName = ss.str(); location = glGetUniformLocation(shader, uniformName.c_str()); if(location == -1) { return false; } glUniform1i(location, (GLint)m_DrawFrameContext.m_lights[i].m_lightAngleAttenCurveType); ss.clear(); ss.seekp(0); ss << "allLights[" << i << "]." << "lightAngleAttenCurveParams" << ends; uniformName = ss.str(); location = glGetUniformLocation(shader, uniformName.c_str()); if(location == -1) { return false; } glUniform1fv(location, 5, m_DrawFrameContext.m_lights[i].m_lightAngleAttenCurveParams); } return true;}
這裡要注意在OpenGL當中傳遞數組的方法。在高版本的OpenGL當中我們可以直接傳遞一塊Buffer給Shader,但是在較低版本的OpenGL當中我們仍然需要這樣顯式的一個一個傳遞。我們使用了C++標準庫當中的ostringstream對象來生成正確的OpenGL uniform constant的變數名,但是使用的時候需要注意正確重置ostringstream對象的方法。
最後我們在Blender當中加入幾個新的光源,然後導出OGEX文件給我們的程序使用。在我的例子當中,我是增加了一個射燈(紅色)和一個點光源(藍色),並且把原來的射燈的顏色改為(綠色),最終執行的效果如下:
https://www.zhihu.com/video/975778383724032000參考引用:
Modern OpenGL 08 - Even More Lighting: Directional Lights, Spotlights, & Multiple Lights※WebGL 和 OpenGL ES 有什麼區別?
※初學OpenGL 用的是OpenGL2.1加MFC 但是感覺好難啊!難度是兩方面的啊?
※如何實現魚眼效果?
※OpenGL ES 的精度問題
※如何利用shader在文字上添加漸變陰影的效果?