首页 > 其他分享 >Shadow Mapping (Games202)

Shadow Mapping (Games202)

时间:2023-08-27 23:45:16浏览次数:41  
标签:Games202 1.0 light float Mapping 缓冲 Shadow gl

Shadow Mapping (Games202)

2-Pass Algorithm

Pass 1. Render from Light

Pass1需要知道光线能照射到的点,也就是从光源所在的视角去渲染模型,有哪些是能被渲染出,哪些会被遮挡住而不被渲染。

1692850854309

我们知道深度测试所做的就是从Camera View去看哪些点是能被看到,忽略那些被遮挡的点。所以在这里判断光线是否能照射到点也用到深度测试。在OpenGL里,我们需要利用帧缓冲(FrameBuffer)参考这篇帧缓冲文章)绑定一个从Light View看到的画面,这帧缓冲绑定一个纹理作为颜色缓冲,还绑定了一个深度缓冲用以深度测试。用来渲染这个帧缓冲的Shader会根据深度测试将光源能看见的点渲染到颜色缓冲中,我需要修改FragmentShader使得颜色缓冲输出的颜色和深度Pack在一起。我们创建的帧缓冲并不会直接影响屏幕上的输出,只有默认帧缓冲才会直接影响屏幕输出。

在作业1框架的 FBO.js文件中绑定了颜色缓冲和深度缓冲,这样我们可以得到从光源视角看到的模型,也就是模拟了光线照射到的地方。

//FBO.js
    class FBO{
        constructor(gl){
            var framebuffer, texture, depthBuffer;

            //定义错误函数

            ...
  
            //创建帧缓冲区对象
            framebuffer = gl.createFramebuffer();

            ...
  
            //创建纹理对象并设置其尺寸和参数
            texture = gl.createTexture();
  
            ...

            framebuffer.texture = texture;//将纹理对象存入framebuffer
  
            //创建渲染缓冲区对象并设置其尺寸和参数
            depthBuffer = gl.createRenderbuffer();

            ...
  
            gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
            gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, resolution, resolution);
  
            //将纹理和渲染缓冲区对象关联到帧缓冲区对象上
            gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
            gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);//深度缓冲
            gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER,depthBuffer);//颜色缓冲
  
            //检查帧缓冲区对象是否被正确设置
            ...
  
            //取消当前的focus对象
            ...
  
            return framebuffer;
        }
    }

DirectionalLight.js文件中给光源light绑定一个帧缓冲FBO

//DirectionalLight.js
class DirectionalLight {

    constructor(lightIntensity, lightColor, lightPos, focalPoint, lightUp, hasShadowMap, gl) {
        ...

        this.fbo = new FBO(gl);//给光源绑定一个缓冲帧

        ...
        }
    }

    CalcLightMVP(translate, scale) {

        ...

        return lightMVP;//转换到Light View的MVP矩阵
    }
}

loadOBJ.js文件中将light传入ShadowMaterial中,这样ShadowMaterial就能绑定light的 FBOlightMVP。这里创建的material是camera view,绑定默认的缓冲帧。而shadowMaterial则是light view,绑定我们创建的缓冲帧。

//loadOBJ.js 51行
case 'PhongMaterial':
	material = buildPhongMaterial(colorMap, mat.specular.toArray(), light, Translation, Scale, "./src/shaders/phongShader/phongVertex.glsl", "./src/shaders/phongShader/phongFragment.glsl");
	shadowMaterial = buildShadowMaterial(light, Translation, Scale, "./src/shaders/shadowShader/shadowVertex.glsl", "./src/shaders/shadowShader/shadowFragment.glsl");
//ShadowMaterial.js
class ShadowMaterial extends Material {

    constructor(light, translate, scale, vertexShader, fragmentShader) {
        let lightMVP = light.CalcLightMVP(translate, scale);

        super({
            'uLightMVP': { type: 'matrix4fv', value: lightMVP }//绑定lightMVP
        }, [], vertexShader, fragmentShader, light.fbo);//绑定了FBO
    }
}

async function buildShadowMaterial(light, translate, scale, vertexPath, fragmentPath) {


    let vertexShader = await getShaderString(vertexPath);
    let fragmentShader = await getShaderString(fragmentPath);

    return new ShadowMaterial(light, translate, scale, vertexShader, fragmentShader);

}

MeshRender.js中,根据是默认帧缓冲还是我们创建的帧缓冲来使用 gl.viewport。这里一定要记得调用glViewport。因为阴影贴图经常和我们原来渲染的场景(通常是窗口分辨率)有着不同的分辨率,我们需要改变视口(viewport)的参数以适应阴影贴图的尺寸。如果我们忘了更新视口参数,最后的深度贴图要么太小要么就不完整。根据material的不同会采用不同的shader来渲染。

//MeshRender.js

class MeshRender {

	#vertexBuffer;
	#normalBuffer;
	#texcoordBuffer;
	#indicesBuffer;

	constructor(gl, mesh, material) {

		this.gl = gl;
		this.mesh = mesh;
		this.material = material;

		...
	}


	...


	draw(camera) {
		const gl = this.gl;

		gl.bindFramebuffer(gl.FRAMEBUFFER, this.material.frameBuffer);
		if (this.material.frameBuffer != null) {//如果为null就是默认的帧缓冲,是camera view
			// Shadow map
			gl.viewport(0.0, 0.0, resolution, resolution);//light view
		} else {
			gl.viewport(0.0, 0.0, window.screen.width, window.screen.height);
		}

		gl.useProgram(this.shader.program.glShaderProgram);//使用shader

		...

		// Draw
		{
			const vertexCount = this.mesh.count;
			const type = gl.UNSIGNED_SHORT;
			const offset = 0;
			gl.drawElements(gl.TRIANGLES, vertexCount, type, offset);
		}
	}
}

首先渲染的是shadowMaterial,来看fragment shader。首先这不是在默认缓冲帧上的渲染,因此不会对屏幕产生直接影响。由于帧缓冲开启了深度测试,因此最终渲染的点都会是light view能看见的点,也就是光线能打到的点。这些点的深度就是 gl_FragCoord.z,由于最终输出格式是vec4的颜色值,需要利用 pack()进行格式的变换。最终,深度值被存储到颜色缓冲中,在前文中说过,在非默认FBO中颜色缓冲实际是一个纹理图,那么在Pass2的过程中就需要去采样这个纹理,然后 Unpack出其中的深度值。

//shadowFragment.glsl

#ifdef GL_ES
precision mediump float;
#endif

uniform vec3 uLightPos;
uniform vec3 uCameraPos;

varying highp vec3 vNormal;
varying highp vec2 vTextureCoord;

vec4 pack (float depth) {
    // 使用rgba 4字节共32位来存储z值,1个字节精度为1/256
    const vec4 bitShift = vec4(1.0, 256.0, 256.0 * 256.0, 256.0 * 256.0 * 256.0);
    const vec4 bitMask = vec4(1.0/256.0, 1.0/256.0, 1.0/256.0, 0.0);
    // gl_FragCoord:片元的坐标,fract():返回数值的小数部分
    vec4 rgbaDepth = fract(depth * bitShift); //计算每个点的z值
    rgbaDepth -= rgbaDepth.gbaa * bitMask; // Cut off the value which do not fit in 8 bits
    return rgbaDepth;
}

void main(){

  //gl_FragColor = vec4( 1.0, 0.0, 0.0, gl_FragCoord.z);
  gl_FragColor = pack(gl_FragCoord.z);
}

Pass 2. Render from Eye

Pass 2的步骤很简单,在Pass 1中得到了一个保存了光线能照射到的点的Shadow Map。我们从Camera View也去看每个能看到的点,将这些点转换到Light View的视角,去对比这些点的Z值和Shadow Map中对应的位置的值的大小,如果大于Shadow Map中对应位置的值,就说明光线照射不到这个点。如下图所示。

1692862591540

在Pass 1的部分我们知道,Shadow Map实际上是一个Pack了Z值的纹理图,这个纹理绑定在light的FBO中。

PhongMaterial.js中,可以看到这个light的FBO传给了PhongMaterial

class PhongMaterial extends Material {

    constructor(color, specular, light, translate, scale, vertexShader, fragmentShader) {
        let lightMVP = light.CalcLightMVP(translate, scale);
        let lightIntensity = light.mat.GetIntensity();

        super({
            // Phong
            'uSampler': { type: 'texture', value: color },
            'uKs': { type: '3fv', value: specular },
            'uLightIntensity': { type: '3fv', value: lightIntensity },
            // Shadow
            'uShadowMap': { type: 'texture', value: light.fbo },//绑定了FBO,也就能获得Shadow Map纹理
            'uLightMVP': { type: 'matrix4fv', value: lightMVP },

        }, [], vertexShader, fragmentShader);
    }
}

MeshRender.js中给shader绑定这个texture。

//144行
bindMaterialParameters() {
		const gl = this.gl;

		let textureNum = 0;
		for (let k in this.material.uniforms) {

			...


			} else if (this.material.uniforms[k].type == 'texture') {
				gl.activeTexture(gl.TEXTURE0 + textureNum);
				gl.bindTexture(gl.TEXTURE_2D, this.material.uniforms[k].value.texture);
				gl.uniform1i(this.shader.program.uniforms[k], textureNum);
				textureNum += 1;
			}
		}
	}

PhongFragment.glsl里面我们采样出纹理值,然后 Unpack出Z值。

float useShadowMap(sampler2D shadowMap, vec4 shadowCoord){
  //执行透视除法
  shadowCoord = shadowCoord * 0.5 + 0.5;
  float closestDepth = unpack(texture2D(shadowMap, shadowCoord.xy).rgba);
  float currentDepth = shadowCoord.z;
  if(closestDepth < currentDepth)
  {
    return 0.0;
  }
  return 1.0;
}

因为来自深度贴图的深度在0到1的范围,我们也打算使用shadowCoords从深度贴图中去采样,所以我们将NDC坐标变换为0到1的范围。

最终效果

1693028275188

1693031837066

存在一些自遮挡的问题,可以参考自适应Shadow Bias算法

PCF(Percentage Closer Filter)

PCF的作用是对Shadow边界处的反走样处理。在 PhongFragment.glsl里的 main函数里我们可以看到变量 visibility,他就是Camera View看到的点是否在阴影里的判断依据,当 visibility = 0时表示在阴影里面。PCF的做的就是对 visibility做平均处理。

void main(void) {

  float visibility;
  vec3 shadowCoord = vPositionFromLight.xyz / vPositionFromLight.w;
  visibility = useShadowMap(uShadowMap, vec4(shadowCoord, 1.0));
  //visibility = PCF(uShadowMap, vec4(shadowCoord, 1.0));
  //visibility = PCSS(uShadowMap, vec4(shadowCoord, 1.0));

  vec3 phongColor = blinnPhong();

  gl_FragColor = vec4(phongColor * visibility, 1.0);
  //gl_FragColor = vec4(phongColor, 1.0);
}

简单的采样周围的点

最简单的做法就是用当前点的currentDepth对当前点及其周围八个点的closestDepth比较的结果做一个平均:

//PhongFragment.glsl
float PCF(sampler2D shadowMap, vec4 coords) {
  float visibility = 0.0;
  float texelSize = 1.0 / 2048.0; // 单独纹理像素的大小
  coords = coords * 0.5 + 0.5;
  float currentDepth = coords.z;

  for(int i = -1; i <= 1; i++){
    for(int j = -1; j <= 1; j++)
    {
      float closestDepth = unpack(texture2D(shadowMap, coords.xy + vec2(i, j) * texelSize).rgba);
      visibility += currentDepth > closestDepth? 0.0 : 1.0;
    }
  }
  return visibility /= 9.0;

}

这个texelSizes是单独纹理像素的大小。前文说过Shadow Map是一个纹理图,阅读代码知道它的分辨率是2048 * 2048。用1除以Shadow Map的宽高(都是2048)就能返回一个单独纹理像素的大小,我们用以对纹理坐标进行偏移(由于宽高一样,x和y方向偏移值一样),确保每个新样本来自不同的深度值。这里我们采样得到9个值,它们在投影坐标的x和y值的周围,为阴影阻挡进行测试,并最终通过样本的总数目将结果平均化。

最终结果如下

1693031711742

1693031778400

用采样函数随机采样

float PCF(sampler2D shadowMap, vec4 coords) {
  float visibility = 0.0;
  float texelSize = 1.0 / 2048.0; // 单独纹理像素的大小
  coords = coords * 0.5 + 0.5;
  float currentDepth = coords.z;

  //泊松圆盘采样
  poissonDiskSamples(coords.xy);
  //均匀圆盘采样
  //uniformDiskSamples(coords.xy);

  for(int i=0; i<NUM_SAMPLES; i++)
  {
    float closestDepth = unpack(texture2D(shadowMap, coords.xy + poissonDisk[i] * texelSize).rgba);
    visibility += (currentDepth > closestDepth? 0.0 : 1.0);
  }
  
  return visibility /= float(NUM_SAMPLES);

}
  • 泊松圆盘采样

    1693064145679
    1693064176591

  • 均匀圆盘采样
    1693064328944

PCSS(Percentage Closer Soft Shadows)

PCSS的流程一些资料和博客介绍的会很清楚:

记录一点自己的理解,在做PCF的时候我们会在Shadow Map采样Shading Point周边的点做一个滤波,可以看下CNN 基础知识 - 卷积 (Convolution) 填充 (Padding) 步长 (Stride)

采样时会基于单位步长 float texelSize = 1.0 / 2048.0;。2048是Shadow Map纹理的宽和高的分辨率,所以 texelSize就是单位纹理像素的大小。在我们采样周边点时,这个就是步长的基本单位。

在做PCSS时,我们需要计算出半影(Penumbra)的大小,Penumbra乘以texelSize用以调整采样的步长大小,也就是调整滤波核的stride, 如果我们增加采样点数NUM_SAMPLES,相当于增大滤波核的大小。在这篇博客里有对步长和卷积比较详细的分析。

接下来是代码和效果:

float PCSS(sampler2D shadowMap, vec4 coords){

  // STEP 1: avgblocker depth
  coords = coords * 0.5 + 0.5;
  float zReceiver = coords.z;
  float d_Blocker = findBlocker(shadowMap, coords.xy, zReceiver);

  // STEP 2: penumbra size
  float w_light = 20.;
  float w_Penumbra = (zReceiver - d_Blocker) * w_light / d_Blocker;

  // STEP 3: filtering
  float texelSize = 1.0 / 2048.0; // 单独纹理像素的大小
  float visibility = 0.0;
  float currentDepth = coords.z;
  //poissonDiskSamples(coords.xy); //已经在findBlocker中做了
  for(int i=0; i<NUM_SAMPLES; i++)
  {
    float closestDepth = unpack(texture2D(shadowMap, coords.xy + poissonDisk[i] * w_Penumbra * texelSize).rgba);//w_Penumbra调整步长大小
    visibility += (currentDepth > closestDepth? 0.0 : 1.0);
  }

  
  return visibility /= float(NUM_SAMPLES);

}

我做出来的最终效果:

  • 20个采样点
    1693129572610
  • 50个采样点
    1693129598379
  • 100个采样点
    1693129495836

Reference

标签:Games202,1.0,light,float,Mapping,缓冲,Shadow,gl
From: https://www.cnblogs.com/dogwealth/p/17661142.html

相关文章

  • 用box-shadow实现发光效果
    思路大概是:1.设置一个暗的背景(这样才能看到发光)2.设置box-shadow 注意:第一个值,如果没有指定inset,默认阴影在边框外,即阴影向外扩散。使用 inset 关键字会使得阴影落在盒子内部,这样看起来就像是内容被压低了。此时阴影会在边框之内(即使是透明边框)、背景之上、内容之下先看......
  • elasticsearch创建索引带mappings和settings
    一、通过kabana控制台创建我们在kabana控制台创建一个record_feature_tag的索引,对应的mapping配置如下PUT/record_feature_tag{"mappings":{"properties":{"_class":{"type":"keyword"},&quo......
  • 【转载】The Dog and the Shadow
    ArticleFormatSourceChatGPTMainContentSourceTitle:TheCityMouseandtheCountryMouseSource:https://www.zhihu.com/question/263840407/answer/1108124796Author:苏焉儿MainContentIthappenedthataDoghadgotapieceofmeatandwascarryingithom......
  • 什么是Shadowbans?
    围绕影子禁令的概念一直在酝酿审查叙事,影子禁令是指用户在社交平台上不知情的情况下被屏蔽。在过去的几年里,shadowban这个词已经有了自己的生命,从一种特定的审核技术的象征演变成从实际排名下降到关于硅谷类型试图压制用户声音的毫无根据的阴谋论的简写。“'影子禁令'听起来相当......
  • 什么是Shadowbans?
    围绕影子禁令的概念一直在酝酿审查叙事,影子禁令是指用户在社交平台上不知情的情况下被屏蔽。在过去的几年里,shadowban这个词已经有了自己的生命,从一种特定的审核技术的象征演变成从实际排名下降到关于硅谷类型试图压制用户声音的毫无根据的阴谋论的简写。“'影子禁令'听起来相当邪......
  • @RequestMapping(value = "/testxml", produces = {"application/xml; charset=UTF-8"
    这行代码是使用SpringFramework的注解来配置一个用于处理HTTP请求的方法。具体来说,这是一个用于处理GET请求的方法,路径为"/testxml"。让我为你解释其中的含义:@RequestMapping:这是SpringFramework提供的注解,用于将一个方法映射到特定的请求路径。在这个例子中,它将......
  • Mapping iostat to the node exporter’s node_disk_* metrics
    参考:https://www.robustperception.io/mapping-iostat-to-the-node-exporters-node_disk_-metrics/ Thenodeexporterandtoolslikeiostatandsarusethesamecoredata,buthowdotheyrelatetoeachother? Prometheusmetricnamestendtotieprettydirect......
  • Spring源码分析(五) MappingJackson2HttpMessageConverter
    大家用过springmvc的肯定都用过@RequestBody和@ResponseBody注解吧,你了解这个的原理吗?这篇文章我们就来说下它是怎么实现json转换的。首先来看一个类RequestResponseBodyMethodProcessor,这个类继承了AbstractMessageConverterMethodProcessor,我们来看看这个类的构造方法protec......
  • spring-mvc系列:详解@RequestMapping注解(value、method、params、header等)
    目录一、@RequestMapping注解的功能二、@RequestMapping注解的位置三、@RequestMapping注解的value属性四、@RequestMapping注解的method属性五、@RequestMapping注解的params属性六、@RequestMapping注解的header属性七、SpringMVC支持ant分格的路径八、SpringMVC支持路径中的占......
  • MappingJackson2HttpMessageConverter数据处理
    主键用的雪花算法,值域超过了js的范围……后端返回的日期字段总不是我想要的格式……空值的字段就不要返回了,省点流量吧……试试换成自己的MappingJackson2HttpMessageConverter呗Talkischeap,showyouthecode!importcom.fasterxml.jackson.annotation.JsonInclude;importco......