獲取示例代碼
本文將為大家介紹3D實(shí)時(shí)陰影技術(shù)ShadowMap,又稱為陰影貼圖技術(shù)。下面是效果圖。
原理介紹
我們?cè)诹私釹hadowMap這項(xiàng)技術(shù)之前,先來(lái)了解一下什么是陰影。在現(xiàn)實(shí)生活中,陰影伴隨著光而生。下面這張圖演示了在平行光下,影子是如何形成的。
圖中的兩個(gè)物體,在光照方向上,距離光源最近的那些點(diǎn)會(huì)被照亮,之后的點(diǎn)則不會(huì)接受到光照。所以判斷一個(gè)點(diǎn)是否在陰影中的關(guān)鍵就是,它是否是光照方向上距離光源最近的點(diǎn)。
深度緩沖區(qū)
之前我們說(shuō)過(guò)深度緩沖區(qū),他可以用來(lái)判斷像素是否需要被寫入顏色緩沖區(qū)。它的原理如下。
- 初始化所有的深度緩沖區(qū)數(shù)據(jù)為1。
- 系統(tǒng)在每個(gè)像素寫入顏色緩沖區(qū)之前都會(huì)比較它的深度值和當(dāng)前深度緩沖區(qū)對(duì)應(yīng)的深度值,如果小于當(dāng)前深度緩沖區(qū)對(duì)應(yīng)的深度值,就寫入該像素,并將當(dāng)前深度緩沖區(qū)對(duì)應(yīng)的深度值改為該像素的深度值。否則,丟棄該像素。
所有的深度值在比較前都會(huì)被規(guī)范化成0~1,投影矩陣近平面上的深度為0,遠(yuǎn)平面上的深度為1。最終保留下來(lái)的都是深度最小的像素點(diǎn)。我們可以利用這個(gè)特性來(lái)記錄光照方向上距離光源最近的點(diǎn)。
深度紋理
我們?nèi)绻褂眉y理來(lái)充當(dāng)深度緩沖區(qū),那么我們將得到深度紋理。深度紋理是我們找到光照方向上距離光源最近的點(diǎn)的好幫手。假設(shè)我們?cè)诠庠闯龇胖靡粋€(gè)攝像機(jī),觀察方向和光源方向調(diào)整為一致。然后使用正交投影矩陣進(jìn)行投影。接著使用這個(gè)攝像機(jī)渲染場(chǎng)景。那么深度紋理中就會(huì)記錄一系列距離光源最近的像素點(diǎn)的深度值。
使用深度紋理
我們有了深度紋理,那么如何使用它呢?在使用它之前我們需要明白像素點(diǎn)的坐標(biāo)和深度紋理UV的對(duì)應(yīng)關(guān)系。當(dāng)我們使用光源的VP矩陣和模型矩陣變換像素點(diǎn)的原始坐標(biāo)之后,坐標(biāo)值便落在了-1到1之間。我們把坐標(biāo)加上1再乘以0.5,xy就可以當(dāng)做UV使用。使用xy在深度紋理上取值,就可以得到這個(gè)像素點(diǎn)所對(duì)應(yīng)的距離光源最近的點(diǎn)的深度值。將這個(gè)深度值與前面經(jīng)過(guò)處理的像素坐標(biāo)的z軸值比較,就可以判斷該像素點(diǎn)是否在陰影中。
Fragment Shader
為了配合Shadow Map,F(xiàn)ragment Shader需要做以下改變。
- 添加深度紋理和光源處的VP矩陣。
uniform mat4 lightMatrix;
uniform sampler2D shadowMap;
- 獲取深度紋理上對(duì)應(yīng)的深度值并和當(dāng)前深度值做判斷,如果當(dāng)前深度比距離光源最近的點(diǎn)深度大,則在陰影中,否則不在。經(jīng)過(guò)處理的像素坐標(biāo)的z軸可以作為當(dāng)前深度值。它的范圍在0到1之間。
float shadow = 0.0;
vec4 positionInLightSpace = lightMatrix * modelMatrix * vec4(fragPosition, 1.0);
positionInLightSpace /= positionInLightSpace.w;
positionInLightSpace = (positionInLightSpace + 1.0) * 0.5;
vec2 shadowUV = positionInLightSpace.xy;
if (shadowUV.x >= 0.0 && shadowUV.x <=1.0 && shadowUV.y >= 0.0 && shadowUV.y <=1.0) {
vec4 shadowColor = texture2D(shadowMap, shadowUV);
if (shadowColor.r < positionInLightSpace.z) {
shadow = 0.1;
} else {
shadow = 1.0;
}
}
- 為漫反射顏色加入陰影系數(shù)shadow的影響,shadow為1時(shí),無(wú)影響,值越小,顏色越暗。
vec3 diffuse = diffuseStrength * light.color * texture2D(diffuseMap, fragUV).rgb * light.indensity * shadow;
生成深度紋理
想要生成光源處VP矩陣下的深度紋理,需要以下操作。
生成帶有深度紋理的Framebuffer。
- (void)createShadowMap {
self.shadowMapSize = CGSizeMake(1024, 1024);
glGenFramebuffers(1, &shadowMapFramebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, shadowMapFramebuffer);
// 生成深度緩沖區(qū)的紋理對(duì)象并綁定到framebuffer上
glGenTextures(1, &shadowDepthMap);
glBindTexture(GL_TEXTURE_2D, shadowDepthMap);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, self.shadowMapSize.width, self.shadowMapSize.height, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT , GL_TEXTURE_2D, shadowDepthMap, 0);
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
// framebuffer生成失敗
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
將GL_DEPTH_COMPONENT
深度格式的紋理貼圖作為Framebuffer的深度緩沖區(qū)附件,所有渲染到深度緩沖區(qū)的數(shù)據(jù)都會(huì)被寫入shadowDepthMap
。
使用光源的VP渲染場(chǎng)景到上面的Framebuffer。因?yàn)槲覀冎恍枰疃葦?shù)據(jù),所以可以新建一個(gè)Fragment Shader來(lái)渲染場(chǎng)景。一個(gè)空的Fragment Shader
就足夠了,因?yàn)樯疃葧?huì)默認(rèn)寫入深度緩沖區(qū),不需要任何處理。
precision highp float;
void main(void) {
}
然后使用光源的VP渲染。lightProjectionMatrix
和lightCameraMatrix
分別是光源的投影和觀察矩陣。到此我們就得到了光源空間的深度紋理了。
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
glBindFramebuffer(GL_FRAMEBUFFER, shadowMapFramebuffer);
glViewport(0, 0, self.shadowMapSize.width, self.shadowMapSize.height);
glClearColor(0.7, 0.7, 0.7, 1);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
[self drawObjectsForShadowMap];
...
}
- (void)drawObjectsForShadowMap {
[self.objects enumerateObjectsUsingBlock:^(GLObject *obj, NSUInteger idx, BOOL *stop) {
[self.shadowMapContext active];
[obj.context setUniformMatrix4fv:@"projectionMatrix" value:self.lightProjectionMatrix];
[obj.context setUniformMatrix4fv:@"cameraMatrix" value:self.lightCameraMatrix];
[obj draw:self.shadowMapContext];
}];
}
繪制主場(chǎng)景
接下來(lái)我們來(lái)繪制場(chǎng)景到屏幕。
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
...
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
[self drawObjectsForShadowMap];
[(GLKView *)(self.view) bindDrawable];
glClearColor(0.7, 0.7, 0.7, 1);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
[self drawObjects];
}
在[self drawObjects];
中,我們將深度紋理傳入Fragment Shader,而且還要傳入光源的VP矩陣。
- (void)drawObjects {
[self.objects enumerateObjectsUsingBlock:^(GLObject *obj, NSUInteger idx, BOOL *stop) {
[obj.context active];
[obj.context setUniform1f:@"elapsedTime" value:(GLfloat)self.elapsedTime];
[obj.context setUniformMatrix4fv:@"projectionMatrix" value:self.projectionMatrix];
[obj.context setUniformMatrix4fv:@"cameraMatrix" value:self.cameraMatrix];
[obj.context setUniform3fv:@"eyePosition" value:self.eyePosition];
[obj.context setUniform3fv:@"light.direction" value:self.light.direction];
[obj.context setUniform3fv:@"light.color" value:self.light.color];
[obj.context setUniform1f:@"light.indensity" value:self.light.indensity];
[obj.context setUniform1f:@"light.ambientIndensity" value:self.light.ambientIndensity];
[obj.context setUniform3fv:@"material.diffuseColor" value:self.material.diffuseColor];
[obj.context setUniform3fv:@"material.ambientColor" value:self.material.ambientColor];
[obj.context setUniform3fv:@"material.specularColor" value:self.material.specularColor];
[obj.context setUniform1f:@"material.smoothness" value:self.material.smoothness];
[obj.context setUniform1i:@"useNormalMap" value:self.useNormalMap];
[obj.context setUniformMatrix4fv:@"lightMatrix" value:GLKMatrix4Multiply(self.lightProjectionMatrix, self.lightCameraMatrix)];
[obj.context bindTextureName:shadowDepthMap to:GL_TEXTURE2 uniformName:@"shadowMap"];
[obj draw:obj.context];
}];
}
到此,陰影貼圖的主要原理和實(shí)現(xiàn)就介紹完了,但是此時(shí)如果你運(yùn)行代碼將會(huì)得到這樣的場(chǎng)景。這是ShadowMap本身的實(shí)現(xiàn)技術(shù)帶來(lái)的問(wèn)題,我將會(huì)在下一篇文章中介紹修復(fù)的方案。