THREEJS中的SAOShader阴影计算

SAO(Scalable Ambient Obscurance):可扩展环境光遮蔽是一种改进的环境光遮蔽技术,它在保持性能的同时,生成更精确和连续的阴影效果。SAO 采用一种基于深度缓冲区的近似算法,可以在不同的场景和分辨率下保持高质量的阴影效果。然而,SAO 相对于 SSAO 更复杂,可能需要更多的计算资源。

import {
  Matrix4,
  Vector2
} from '../../../three/three-core/Three.js';

/**
 * TODO
 */

const SAOShader = {
  defines: {
    'NUM_SAMPLES': 7,
    'NUM_RINGS': 4,
    'NORMAL_TEXTURE': 0,
    'DIFFUSE_TEXTURE': 0,
    'DEPTH_PACKING': 1,
    'PERSPECTIVE_CAMERA': 1
  },
  uniforms: {

    'tDepth': { value: null },
    'tDiffuse': { value: null },
    'tNormal': { value: null },
    'size': { value: new Vector2( 512, 512 ) },

    'cameraNear': { value: 1 },
    'cameraFar': { value: 100 },
    'cameraProjectionMatrix': { value: new Matrix4() },
    'cameraInverseProjectionMatrix': { value: new Matrix4() },

    'scale': { value: 1.0 },
    'intensity': { value: 0.1 },
    'bias': { value: 0.5 },

    'minResolution': { value: 0.0 },
    'kernelRadius': { value: 100.0 },
    'randomSeed': { value: 0.0 }
  },
  vertexShader: /* glsl */`

    varying vec2 vUv;

    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
    }`,

  fragmentShader: /* glsl */`

    #include <common>

    varying vec2 vUv;

    #if DIFFUSE_TEXTURE == 1
    uniform sampler2D tDiffuse;
    #endif

    uniform sampler2D tDepth;

    #if NORMAL_TEXTURE == 1
    uniform sampler2D tNormal;
    #endif

    uniform float cameraNear;
    uniform float cameraFar;
    uniform mat4 cameraProjectionMatrix;
    uniform mat4 cameraInverseProjectionMatrix;

    uniform float scale;
    uniform float intensity;
    uniform float bias;
    uniform float kernelRadius;
    uniform float minResolution;
    uniform vec2 size;
    uniform float randomSeed;

    // RGBA depth

    #include <packing>

    vec4 getDefaultColor( const in vec2 screenPosition ) {
      #if DIFFUSE_TEXTURE == 1
      return texture2D( tDiffuse, vUv );
      #else
      return vec4( 1.0 );
      #endif
    }

    float getDepth( const in vec2 screenPosition ) {
      #if DEPTH_PACKING == 1
      return unpackRGBAToDepth( texture2D( tDepth, screenPosition ) );
      #else
      return texture2D( tDepth, screenPosition ).x;
      #endif
    }

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

    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;
    }

    vec3 getViewNormal( const in vec3 viewPosition, const in vec2 screenPosition ) {
      #if NORMAL_TEXTURE == 1
      return unpackRGBToNormal( texture2D( tNormal, screenPosition ).xyz );
      #else
      return normalize( cross( dFdx( viewPosition ), dFdy( viewPosition ) ) );
      #endif
    }

    float scaleDividedByCameraFar;
    float minResolutionMultipliedByCameraFar;

    float getOcclusion( const in vec3 centerViewPosition, const in vec3 centerViewNormal, const in vec3 sampleViewPosition ) {
      vec3 viewDelta = sampleViewPosition - centerViewPosition;
      float viewDistance = length( viewDelta );
      float scaledScreenDistance = scaleDividedByCameraFar * viewDistance;

      return max(0.0, (dot(centerViewNormal, viewDelta) - minResolutionMultipliedByCameraFar) / scaledScreenDistance - bias) / (1.0 + pow2( scaledScreenDistance ) );
    }

    // moving costly divides into consts
    const float ANGLE_STEP = PI2 * float( NUM_RINGS ) / float( NUM_SAMPLES );
    const float INV_NUM_SAMPLES = 1.0 / float( NUM_SAMPLES );

    float getAmbientOcclusion( const in vec3 centerViewPosition ) {
      // precompute some variables require in getOcclusion.
      scaleDividedByCameraFar = scale / cameraFar;
      minResolutionMultipliedByCameraFar = minResolution * cameraFar;
      vec3 centerViewNormal = getViewNormal( centerViewPosition, vUv );

      // jsfiddle that shows sample pattern: https://jsfiddle.net/a16ff1p7/
      float angle = rand( vUv + randomSeed ) * PI2;
      vec2 radius = vec2( kernelRadius * INV_NUM_SAMPLES ) / size;
      vec2 radiusStep = radius;

      float occlusionSum = 0.0;
      float weightSum = 0.0;

      for( int i = 0; i < NUM_SAMPLES; i ++ ) {
        vec2 sampleUv = vUv + vec2( cos( angle ), sin( angle ) ) * radius;
        radius += radiusStep;
        angle += ANGLE_STEP;

        float sampleDepth = getDepth( sampleUv );
        if( sampleDepth >= ( 1.0 - EPSILON ) ) {
          continue;
        }

        float sampleViewZ = getViewZ( sampleDepth );
        vec3 sampleViewPosition = getViewPosition( sampleUv, sampleDepth, sampleViewZ );
        occlusionSum += getOcclusion( centerViewPosition, centerViewNormal, sampleViewPosition );
        weightSum += 1.0;
      }

      if( weightSum == 0.0 ) discard;

      return occlusionSum * ( intensity / weightSum );
    }

    void main() {
      float centerDepth = getDepth( vUv );
      if( centerDepth >= ( 1.0 - EPSILON ) ) {
        discard;
      }

      float centerViewZ = getViewZ( centerDepth );
      vec3 viewPosition = getViewPosition( vUv, centerDepth, centerViewZ );

      float ambientOcclusion = getAmbientOcclusion( viewPosition );

      gl_FragColor = getDefaultColor( vUv );
      gl_FragColor.xyz *=  1.0 - ambientOcclusion;
    }`

};

export { SAOShader };

代码分析:

这里的getViewZ, getDepth, getViewPosition和getViewNormal与之前ssaoshader的算法基本一致,唯一不同的是getViewNormal与getDepth增加了对贴图的支持;

其中最核心的函数就是:

float getOcclusion( const in vec3 centerViewPosition, const in vec3 centerViewNormal, const in vec3 sampleViewPosition ) {
	vec3 viewDelta = sampleViewPosition - centerViewPosition;
	float viewDistance = length( viewDelta );
	float scaledScreenDistance = scaleDividedByCameraFar * viewDistance;

	return max(0.0, ( dot(centerViewNormal, viewDelta) - minResolutionMultipliedByCameraFar) / scaledScreenDistance - bias ) / ( 1.0 + pow2( scaledScreenDistance ) );
}

这个函数得到了最终值越大,屏幕越黑;
这里面用到了向量减法,点积的计算,向量减法得到的向量的起点是被减向量的起点,终点是减向量的终点。换言之,结果向量的起点与被减向量的起点相同,终点在由被减向量和减向量的差所代表的位置。dot(点积)是一个二元运算,它用于计算两个向量之间的乘积。对于两个向量a和b,它们的点积可以表示为:

a · b = |a| |b| cos(θ)

其中,|a|和|b|表示向量a和b的长度(或模),θ表示a和b之间的夹角。点积的结果是一个标量(即一个单独的数值),它表示了两个向量之间的相似度或相关性。如果a和b之间的夹角越小(即越接近共线),点积的结果就越大;如果它们之间的夹角越大(即越接近垂直),点积的结果就越小。

dot(centerViewNormal, viewDelta)代表取样点和屏幕点的遮挡关系,遮挡越多,最终值越大;

viewDistance代表两点之间的距离,这个值越大,代表两者越远。

minResolutionMultipliedByCameraFar 代表取样点和屏幕点的距离乘以屏幕像素缩放比,距离越远,这个值越大,最终值越小,这个值与cameraFar正相关,也就是cameraFar越大,最终值越小;

scaledScreenDistance代表摄像机(Camera)到屏幕的距离乘以viewDistance值 ,这个值越大,最终值越大,计算这个值要用到scaleDividedByCameraFar,两者正相关,而scaleDividedByCameraFar与cameraFar反相关**,也就是cameraFar越大,最终值越小;**

bias是计算中的偏差(bias),这个值越大,最终值越小;

最后还要除以scaledScreenDistance的二次方,也还是scaledScreenDistance越大,最终值越小;

这里计算还用到两个变量:

scaleDividedByCameraFar = scale / cameraFar;

minResolutionMultipliedByCameraFar = minResolution * cameraFar;

其中scale, minResolution都是参数,具体来说,minResolution是一个用于控制环境光遮蔽计算精度的参数,通常是一个较小的数值(比如0.0002),而cameraFar则是摄像机到场景最远处的距离。

saoScale指的是环境光遮蔽效果的放大倍数。如果saoScale的值较大,则环境光遮蔽的效果将更加明显,看起来更黑暗。如果saoScale的值较小,则环境光遮蔽的效果将更加轻微,看起来更亮。

最终效果如下:

在这里插入图片描述
参数解释:

saoBias: 对应shader中的bias偏差,越小越暗,越准;

saoIntensity: 对应shader中的intensity, 强度,越大越暗;

saoScale: 对应shader中的scale, 调整景深,越大景深越浅,越大越暗,要和cameraFar做运算;

saoKernelRadius: 取样范围,越大取样范围越大;

saoMinResolution: 对应shader中的minResolution, 控制环境光遮蔽计算精度的参数,通常是一个较小的数值(比如0.0002), 越小越黑;

saoBlur: 模糊开关;

saoBlurStdDev: 模糊半径;

saoBlurDepthCutoff: 模糊截止深度

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 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、付费专栏及课程。

余额充值