THREEJS中的SSAOShader阴影计算

SSAO(Screen Space Ambient Occlusion):屏幕空间环境光遮蔽是一种实时计算环境光遮蔽的技术。它通过在屏幕空间内模拟环境光的散射和遮蔽,生成更真实的阴影效果。SSAO 是一种效率较高的技术,通常用于实时渲染,如游戏和 Web 应用程序。

后处理

先理解一个概念:后处理

后处理(Post-processing)是指在 3D 场景的渲染过程完成之后,对生成的 2D 图像进行额外处理的过程。这种处理可以增强视觉效果,制造各种视觉效果,如模糊、泛光、色调映射、屏幕空间环境光遮蔽(SSAO)等。后处理通常用于提高渲染质量,增强视觉体验,但可能会对性能产生一定影响。

在 Three.js 中,后处理通常使用 EffectComposer 类来实现。EffectComposer 类允许你将多个后处理通道(称为 Pass)组合在一起,以便在渲染场景后按顺序应用它们。通常,你可以在 Three.js 的 examples/jsm/postprocessing 目录中找到许多预定义的后处理通道,如 RenderPass、BloomPass、ShaderPass 等。

FullScreenQuad

然后再讲一个概念:FullScreenQuad

FullScreenQuad 是一种在 Three.js 中常用的后期处理技术。它使用一个占据整个屏幕的四边形(通常是一个矩形)作为渲染目标。这个四边形覆盖了整个屏幕,所以称为 "FullScreen"。FullScreenQuad 主要用于将特定的着色器效果应用于整个屏幕,例如屏幕空间环境光遮蔽(SSAO)、泛光、色调映射等。

FullScreenQuad 的工作原理是将一个带有自定义着色器的材质应用于一个覆盖整个屏幕的矩形几何体。在渲染时,此矩形几何体作为一个单独的渲染通道被绘制到屏幕上。通过将场景渲染到纹理中,然后在后期处理阶段将该纹理应用于 FullScreenQuad,可以实现各种复杂的视觉效果。

在 Three.js 的 examples/jsm/postprocessing 库中,FullScreenQuad 类可以帮助你快速创建一个全屏四边形。

代码如下:

const _camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );


// https://github.com/mrdoob/three.js/pull/21358


const _geometry = new BufferGeometry();
_geometry.setAttribute( 'position', new Float32BufferAttribute( [ - 1, 3, 0, - 1, - 1, 0, 3, - 1, 0 ], 3 ) );
_geometry.setAttribute( 'uv', new Float32BufferAttribute( [ 0, 2, 0, 0, 2, 0 ], 2 ) );


class FullScreenQuad {


	constructor( material ) {


		this._mesh = new Mesh( _geometry, material );


	}


	dispose() {


		this._mesh.geometry.dispose();


	}


	render( renderer ) {


		renderer.render( this._mesh, _camera );


	}


	get material() {


		return this._mesh.material;


	}


	set material( value ) {


		this._mesh.material = value;


	}


}

后处理使用全屏的 PlaneGeometry 来实现全屏的片元着色器,这可能导致四边形的过度渲染。这里使用一个全屏的三角形来代替。

quad overshading(四边形过度渲染)是指在渲染四边形(quad,通常由两个三角形组成)时,由于某些原因导致的不必要的渲染。这些原因可能包括屏幕空间的四边形比实际需要的区域大,或者四边形与其他几何形状重叠,从而导致像素被多次处理和渲染。

在全屏后处理中,我们通常使用一个覆盖整个屏幕的四边形来执行片元着色器。然而,在某些情况下,使用全屏四边形可能会导致不必要的像素处理,从而降低渲染性能。为了避免这种过度渲染,可以使用全屏三角形代替四边形。这样,我们只需要渲染一个足够大的三角形来覆盖整个屏幕,而不是两个三角形组成的四边形。这可以减少渲染过程中的像素处理,提高渲染性能。

在 Three.js 中,cameraNear 和 cameraFar 是摄像机(Camera)的两个重要属性,它们决定了摄像机的渲染范围。这两个属性分别表示摄像机视锥体的近裁剪面和远裁剪面的距离。摄像机仅渲染位于这两个裁剪面之间的物体。

cameraNear:近裁剪面距离,表示距离摄像机多近的物体开始被渲染。其值必须为正数。如果物体距离摄像机比 cameraNear 更近,它将不会被渲染。

cameraFar:远裁剪面距离,表示距离摄像机多远的物体停止被渲染。其值必须大于 cameraNear。如果物体距离摄像机比 cameraFar 更远,它将不会被渲染。

在裁剪空间之后是一个名为视口空间(Viewport Space)的坐标系统。视口空间考虑了摄像机的近裁剪面(cameraNear)和远裁剪面(cameraFar),它将裁剪空间中的坐标映射到一个可见的 3D 场景范围。在视口空间中,摄像机的近裁剪面对应于 z 轴上的 0,远裁剪面对应于 z 轴上的 1。

THREEJS中的SSAOShader的完整代码如下:


const _camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );

// https://github.com/mrdoob/three.js/pull/21358

const _geometry = new BufferGeometry();
_geometry.setAttribute( 'position', new Float32BufferAttribute( [ - 1, 3, 0, - 1, - 1, 0, 3, - 1, 0 ], 3 ) );
_geometry.setAttribute( 'uv', new Float32BufferAttribute( [ 0, 2, 0, 0, 2, 0 ], 2 ) );

class FullScreenQuad {

	constructor( material ) {

		this._mesh = new Mesh( _geometry, material );

	}

	dispose() {

		this._mesh.geometry.dispose();

	}

	render( renderer ) {

		renderer.render( this._mesh, _camera );

	}

	get material() {

		return this._mesh.material;

	}

	set material( value ) {

		this._mesh.material = value;

	}

}

代码分析

其中vertexShader顶点着色器没什么好说的。我们重点来看一下fragmentShader。

float getDepth( const in vec2 screenPosition ) {
  return texture2D( tDepth, screenPosition ).x;
}

这个函数是为了获取屏幕上某一点的深度值,通常表示在渲染过程中,场景中某个像素距离相机的距离。深度值的范围取决于深度缓冲区(Depth buffer)的精度以及相机的远近裁剪平面(Near and Far clipping planes)。

在计算机图形学中,深度值通常被归一化到 [0, 1] 的范围内。其中,0 表示像素位于相机的近裁剪平面上,1 表示像素位于相机的远裁剪平面上。实际的深度值会在这个范围内根据像素距离相机的距离进行线性或非线性插值。需要注意的是,深度值在 [0, 1] 范围内的分布通常是非线性的,这意味着距离相机较近的像素深度值变化较大,而距离相机较远的像素深度值变化较小。这是因为非线性深度缓冲区有助于减少深度冲突(Z-fighting)现象,从而提高渲染质量。在 Three.js 中,默认情况下深度值是非线性的。这是因为 OpenGL/WebGL 在将摄像机空间中的坐标转换为裁剪空间坐标时使用了透视除法。透视除法会导致裁剪空间中的深度值呈现非线性分布。这有助于提高渲染质量,尤其是在处理深度冲突(Z-fighting)现象时。

float getViewZ( const in float depth ) {
	#if PERSPECTIVE_CAMERA == 1
		return perspectiveDepthToViewZ( depth, cameraNear, cameraFar );
	#else
		return orthographicDepthToViewZ( depth, cameraNear, cameraFar );
	#endif
}

这是一个GLSL函数,根据深度值(depth)计算视图空间中的Z坐标。该函数考虑了摄像机类型(透视摄像机或正交摄像机),因为不同类型的摄像机在计算深度时有所不同。

getViewZ 函数返回的值表示视图空间中的 Z 坐标。视图空间是将场景中的物体变换到摄像机坐标系中的空间,因此 Z 坐标表示物体距离摄像机的距离。返回值的范围取决于摄像机的近裁剪平面(cameraNear)和远裁剪平面(cameraFar)。

对于透视摄像机:

  • 近裁剪平面对应的 Z 值为 -cameraNear。
  • 远裁剪平面对应的 Z 值为 -cameraFar。

对于正交摄像机:

  • 近裁剪平面对应的 Z 值为 -cameraNear。
  • 远裁剪平面对应的 Z 值为 -cameraFar。

请注意,视图空间中的 Z 坐标通常是负数,因为摄像机朝向的方向默认是沿着 -Z 轴。所以,getViewZ 函数返回值的范围大致在 [-cameraFar, -cameraNear] 之间。这个范围内的值表示了物体在视图空间中距离摄像机的距离。

vec3 getViewPosition( const in vec2 screenPosition, const in float depth, const in float viewZ ) {


	float clipW = cameraProjectionMatrix[2][3] * viewZ + cameraProjectionMatrix[3][3];


	vec4 clipPosition = vec4( ( vec3( screenPosition, depth ) - 0.5 ) * 2.0, 1.0 );


	clipPosition *= clipW; // unprojection.


	return ( cameraInverseProjectionMatrix * clipPosition ).xyz;


}

这是一个GLSL函数,用于将屏幕空间中的点(screenPosition 和 depth)转换为视图空间中的位置(三维坐标)。它需要深度值(depth)和由 getViewZ 函数计算得到的视图空间中的 Z 坐标(viewZ)作为输入参数。这个过程涉及将屏幕空间中的点还原到裁剪空间,然后使用相机的逆投影矩阵将其转换到视图空间。

vec3 getViewNormal( const in vec2 screenPosition ) {
	return unpackRGBToNormal( texture2D( tNormal, screenPosition ).xyz );
}

这是一个GLSL(OpenGL Shading Language)函数,用于获取屏幕空间中给定位置的表面法线。它的作用是从屏幕空间位置(screenPosition)中获取法线贴图(tNormal)的纹理数据,并将其转换为一个法线向量。

vec3 tangent = normalize( random - viewNormal * dot( random, viewNormal ) );

我们得到一个与表面法线(viewNormal)正交的切线向量(tangent)

vec3 bitangent = cross( viewNormal, tangent );

计算表面法线向量(viewNormal)和切线向量(tangent)之间的叉积。叉积是一个新的向量,与输入向量都垂直(正交)。

vec4 samplePointNDC = cameraProjectionMatrix * vec4( samplePoint, 1.0 );:将视图空间中的采样点与投影矩阵(cameraProjectionMatrix)相乘,将采样点从视图空间投影到裁剪空间。vec4( samplePoint, 1.0 ) 将三维向量 samplePoint 转换为齐次坐标,这是在将点投影到裁剪空间时所需的。

samplePointNDC /= samplePointNDC.w;:将裁剪空间中的采样点坐标除以其齐次坐标中的 w 分量,将其从裁剪空间转换为归一化设备坐标空间。归一化设备坐标空间中的坐标范围在 [-1, 1] 区间内。这一步完成了所谓的透视除法,它将齐次坐标转换为标准的三维坐标。

vec2 samplePointUv = samplePointNDC.xy * 0.5 + 0.5; 这行GLSL代码将归一化设备坐标(NDC)空间中的采样点坐标转换为屏幕空间的UV坐标。

float getLinearDepth( const in vec2 screenPosition ) {


	#if PERSPECTIVE_CAMERA == 1


		float fragCoordZ = texture2D( tDepth, screenPosition ).x;
		float viewZ = perspectiveDepthToViewZ( fragCoordZ, cameraNear, cameraFar );
		return viewZToOrthographicDepth( viewZ, cameraNear, cameraFar );


	#else


		return texture2D( tDepth, screenPosition ).x;


	#endif


}

这是一个GLSL函数,用于根据屏幕位置(screenPosition)获取线性深度。这个函数根据当前相机类型是透视相机还是正交相机,采取不同的方式获取线性深度值。

float occlusion = 0.0;


for ( int i = 0; i < KERNEL_SIZE; i ++ ) {


  vec3 sampleVector = kernelMatrix * kernel[ i ]; // reorient sample vector in view space
  vec3 samplePoint = viewPosition + ( sampleVector * kernelRadius ); // calculate sample point
  
  vec4 samplePointNDC = cameraProjectionMatrix * vec4( samplePoint, 1.0 ); // project point and calculate NDC
  samplePointNDC /= samplePointNDC.w;
  
  vec2 samplePointUv = samplePointNDC.xy * 0.5 + 0.5; // compute uv coordinates
  
  float realDepth = getLinearDepth( samplePointUv ); // get linear depth from depth texture
  float sampleDepth = viewZToOrthographicDepth( samplePoint.z, cameraNear, cameraFar ); // compute linear depth of the sample view Z value
  float delta = sampleDepth - realDepth;


  if ( delta > minDistance && delta < maxDistance ) { // if fragment is before sample point, increase occlusion
  
  	occlusion += 1.0;
  
  }


}


occlusion = clamp( occlusion / float( KERNEL_SIZE ), 0.0, 1.0 );

samplePoint 和 viewPosition 的 z 值可能会不一样。这两个向量都是在视图空间(view space)中表示的点。

viewPosition 是当前片段(fragment)在视图空间中的位置。在计算环境光遮蔽(Ambient Occlusion)时,viewPosition 通常是我们要计算遮蔽度的点。

samplePoint 是一个采样点,它基于 viewPosition 和一个采样向量(sample vector)计算得到。这个采样向量来自于预先定义的采样核(sampling kernel),经过变换后与当前片段的法线、切线和副切线对齐。然后将变换后的采样向量乘以一个核半径(kernel radius)并加到 viewPosition 上,从而得到采样点的位置。由于采样向量的方向和大小可能有所不同,samplePoint 的 z 值可能与 viewPosition 的 z 值不同。

在环境光遮蔽计算中,我们通常会检查 samplePoint 和 viewPosition 之间的深度差异,以确定当前片段是否受到其他几何体的遮挡。 如果受到遮挡,则会增加遮蔽值occlusion。

gl_FragColor = vec4( vec3( 1.0 - occlusion ), 1.0 );

最后将物体亮度减去遮蔽值,就得到近似的阴影效果;

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
要创建一个带有阴影的立方体,可以使用Three.js的DirectionalLight类和SpotLight类来创建光源,使用MeshStandardMaterial类来创建材质,并将场景的物体设置为接收阴影和投射阴影。 下面是一个示例代码: ```javascript // 导入Three.js库 import * as THREE from 'three'; // 创建场景 const scene = new THREE.Scene(); // 创建相机 const camera = new THREE.PerspectiveCamera( 75, // 视角 window.innerWidth / window.innerHeight, // 宽高比 0.1, // 近裁剪面 1000 // 远裁剪面 ); // 创建渲染器 const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); renderer.shadowMap.enabled = true; // 启用阴影 document.body.appendChild(renderer.domElement); // 创建立方体 const geometry = new THREE.BoxGeometry(); const material = new THREE.MeshStandardMaterial({ color: 0x00ff00, roughness: 0.5, metalness: 0.5 }); const cube = new THREE.Mesh(geometry, material); cube.castShadow = true; // 投射阴影 cube.receiveShadow = true; // 接收阴影 scene.add(cube); // 创建光源 const directionalLight = new THREE.DirectionalLight(0xffffff, 1); directionalLight.position.set(0, 10, 0); directionalLight.castShadow = true; // 投射阴影 scene.add(directionalLight); const spotLight = new THREE.SpotLight(0xffffff, 1); spotLight.position.set(0, 10, 0); spotLight.castShadow = true; // 投射阴影 scene.add(spotLight); // 设置相机位置 camera.position.z = 5; // 渲染场景 function animate() { requestAnimationFrame(animate); cube.rotation.x += 0.01; cube.rotation.y += 0.01; renderer.render(scene, camera); } animate(); ``` 在上面的代码,我们使用MeshStandardMaterial类创建立方体的材质,并将其属性设置为启用阴影。然后,我们将立方体设置为投射阴影和接收阴影。我们还使用DirectionalLight类和SpotLight类创建光源,并将其设置为投射阴影。最后,我们使用requestAnimationFrame()函数循环渲染场景,产生动画效果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值