├── .DS_Store ├── .gitignore ├── AssetManager.js ├── EffectShader.js ├── TranslucentMaterial.js ├── dragon.glb ├── dragonthicc.glb ├── index.html ├── main.js ├── skybox ├── Box_Back.bmp ├── Box_Bottom.bmp ├── Box_Front.bmp ├── Box_Left.bmp ├── Box_Right.bmp └── Box_Top.bmp └── stats.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/N8python/subsurfaceScatteringMat/47c7bbc6a0e29cadff85b951a2c52e1e315fb32f/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /AssetManager.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.skypack.dev/three@0.152.0'; 2 | import { 3 | GLTFLoader 4 | } from 'https://unpkg.com/three@0.152.0/examples/jsm/loaders/GLTFLoader.js'; 5 | const AssetManager = {}; 6 | AssetManager.gltfLoader = new GLTFLoader(); 7 | AssetManager.audioLoader = new THREE.AudioLoader(); 8 | AssetManager.loadGLTFAsync = (url) => { 9 | return new Promise((resolve, reject) => { 10 | AssetManager.gltfLoader.load(url, obj => { 11 | resolve(obj); 12 | }) 13 | }); 14 | } 15 | 16 | AssetManager.loadAudioAsync = (url) => { 17 | return new Promise((resolve, reject) => { 18 | AssetManager.audioLoader.load(url, (buffer) => { 19 | resolve(buffer); 20 | }); 21 | }) 22 | } 23 | 24 | AssetManager.loadTextureAsync = (url) => { 25 | return new Promise((resolve, reject) => { 26 | THREE.ImageUtils.loadTexture(url, null, (tex) => { 27 | resolve(tex); 28 | }) 29 | }) 30 | } 31 | AssetManager.loadAll = (promiseArr, element, message) => { 32 | let count = promiseArr.length; 33 | let results = []; 34 | element.innerHTML = `${message} (${promiseArr.length - count} / ${promiseArr.length})...` 35 | return new Promise((resolve, reject) => { 36 | promiseArr.forEach((promise, i) => { 37 | promise.then(result => { 38 | results[i] = result; 39 | count--; 40 | element.innerHTML = `${message} (${promiseArr.length - count} / ${promiseArr.length})...` 41 | if (count === 0) { 42 | resolve(results); 43 | } 44 | }) 45 | }) 46 | }); 47 | } 48 | export { AssetManager }; -------------------------------------------------------------------------------- /EffectShader.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.skypack.dev/three@0.152.0'; 2 | const EffectShader = { 3 | 4 | uniforms: { 5 | 6 | 'sceneDiffuse': { value: null } 7 | }, 8 | 9 | vertexShader: /* glsl */ ` 10 | varying vec2 vUv; 11 | void main() { 12 | vUv = uv; 13 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 14 | }`, 15 | 16 | fragmentShader: /* glsl */ ` 17 | uniform sampler2D sceneDiffuse; 18 | varying vec2 vUv; 19 | void main() { 20 | vec4 diffuse = texture2D(sceneDiffuse, vUv); 21 | gl_FragColor = vec4(diffuse.rgb, 1.0); 22 | }` 23 | 24 | }; 25 | 26 | export { EffectShader }; -------------------------------------------------------------------------------- /TranslucentMaterial.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.skypack.dev/three@0.152.0'; 2 | import { FullScreenQuad } from "https://unpkg.com/three@0.152.0/examples/jsm/postprocessing/Pass.js"; 3 | const translucentHBlur = { 4 | uniforms: { 5 | tDiffuse: { value: null }, 6 | tDepth: { value: null }, 7 | resolution: { value: new THREE.Vector2() }, 8 | size: { value: 4.0 }, 9 | stride: { value: 8.0 }, 10 | near: { value: 0.1 }, 11 | far: { value: 1000.0 }, 12 | orthographic: { value: false }, 13 | logDepthBuffer: { value: false } 14 | }, 15 | vertexShader: /*glsl*/ ` 16 | varying vec2 vUv; 17 | void main() { 18 | vUv = uv; 19 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 20 | } 21 | `, 22 | fragmentShader: /*glsl*/ ` 23 | uniform highp sampler2D tDiffuse; 24 | uniform highp sampler2D tDepth; 25 | uniform float size; 26 | uniform float stride; 27 | uniform float near; 28 | uniform float far; 29 | uniform bool orthographic; 30 | uniform bool logDepthBuffer; 31 | uniform vec2 resolution; 32 | varying vec2 vUv; 33 | highp float linearize_depth(highp float d, highp float zNear,highp float zFar) 34 | { 35 | highp float z_n = 2.0 * d - 1.0; 36 | return 2.0 * zNear * zFar / (zFar + zNear - z_n * (zFar - zNear)); 37 | } 38 | highp float linearize_depth_log(highp float d, highp float nearZ,highp float farZ) { 39 | float depth = pow(2.0, d * log2(farZ + 1.0)) - 1.0; 40 | float a = farZ / (farZ - nearZ); 41 | float b = farZ * nearZ / (nearZ - farZ); 42 | float linDepth = a + b / depth; 43 | return linearize_depth(linDepth, nearZ, farZ); 44 | } 45 | void main() { 46 | vec2 uv = vUv; 47 | float depth = orthographic ? near + (far - near) * texture2D(tDepth, uv).x: 48 | ( 49 | logDepthBuffer ? 50 | linearize_depth_log(texture2D(tDepth, uv).x, near, far) : 51 | linearize_depth(texture2D(tDepth, uv).x, near, far) 52 | ); // Attenuate the stride based on the depth, so that the blur is larger near the camera 53 | float updatedStride = stride * ( 54 | 1.0 / (1.0 + (depth - near)) 55 | ); 56 | vec2 invResolution = 1.0 / resolution; 57 | vec3 color = vec3(0.0); 58 | float total = 0.0; 59 | if (texture2D(tDiffuse, uv).a == 0.0) { 60 | gl_FragColor = vec4(0.0); 61 | return; 62 | } 63 | // Calculate the average of the surrounding pixels, using a horizontal Gaussian filter 64 | // We take advantage of the fact that the Gaussian filter is separable, so we can do two passes 65 | // Weight each pixel by the Gaussian function and its alpha value 66 | for (float x = -size; x <= size; x += 1.0) { 67 | vec4 texel = texture2D(tDiffuse, uv + vec2(x, 0.0)* updatedStride * invResolution); 68 | float weight = exp(-0.5 * (x * x) / (size * size)) * texel.a; 69 | color += max(texel.rgb, vec3(0.0)) * weight; 70 | total += weight; 71 | } 72 | color /= total; 73 | gl_FragColor = vec4(color, 1.0); 74 | }` 75 | }; 76 | const translucentVBlur = { 77 | uniforms: { 78 | tDiffuse: { value: null }, 79 | tDepth: { value: null }, 80 | resolution: { value: new THREE.Vector2() }, 81 | size: { value: 4.0 }, 82 | stride: { value: 8.0 }, 83 | near: { value: 0.1 }, 84 | far: { value: 1000.0 }, 85 | orthographic: { value: false }, 86 | logDepthBuffer: { value: false } 87 | }, 88 | vertexShader: /*glsl*/ ` 89 | varying vec2 vUv; 90 | void main() { 91 | vUv = uv; 92 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 93 | } 94 | `, 95 | fragmentShader: /*glsl*/ ` 96 | uniform highp sampler2D tDiffuse; 97 | uniform highp sampler2D tDepth; 98 | uniform float size; 99 | uniform float stride; 100 | uniform float near; 101 | uniform float far; 102 | uniform bool orthographic; 103 | uniform bool logDepthBuffer; 104 | uniform vec2 resolution; 105 | varying vec2 vUv; 106 | highp float linearize_depth(highp float d, highp float zNear,highp float zFar) 107 | { 108 | highp float z_n = 2.0 * d - 1.0; 109 | return 2.0 * zNear * zFar / (zFar + zNear - z_n * (zFar - zNear)); 110 | } 111 | highp float linearize_depth_log(highp float d, highp float nearZ,highp float farZ) { 112 | float depth = pow(2.0, d * log2(farZ + 1.0)) - 1.0; 113 | float a = farZ / (farZ - nearZ); 114 | float b = farZ * nearZ / (nearZ - farZ); 115 | float linDepth = a + b / depth; 116 | return linearize_depth(linDepth, nearZ, farZ); 117 | } 118 | void main() { 119 | vec2 uv = vUv; 120 | float depth = orthographic ? near + (far - near) * texture2D(tDepth, uv).x: 121 | ( 122 | logDepthBuffer ? 123 | linearize_depth_log(texture2D(tDepth, uv).x, near, far) : 124 | linearize_depth(texture2D(tDepth, uv).x, near, far) 125 | ); 126 | float updatedStride = stride * ( 127 | 1.0 / (1.0 + (depth - near)) 128 | ); 129 | vec2 invResolution = 1.0 / resolution; 130 | vec3 color = vec3(0.0); 131 | float total = 0.0; 132 | if (texture2D(tDiffuse, uv).a == 0.0) { 133 | gl_FragColor = vec4(0.0); 134 | return; 135 | } 136 | // Calculate the average of the surrounding pixels, using a horizontal Gaussian filter 137 | // We take advantage of the fact that the Gaussian filter is separable, so we can do two passes 138 | // Weight each pixel by the Gaussian function and its alpha value 139 | for (float y = -size; y <= size; y += 1.0) { 140 | vec4 texel = texture2D(tDiffuse, uv + vec2(0.0, y) * updatedStride * invResolution); 141 | float weight = exp(-0.5 * (y * y) / (size * size)) * texel.a; 142 | color += max(texel.rgb, vec3(0.0)) * weight; 143 | total += weight; 144 | } 145 | color /= total; 146 | gl_FragColor = vec4(color, 1.0); 147 | 148 | }` 149 | }; 150 | const depthBlit = { 151 | uniforms: { 152 | tDiffuse: { value: null }, 153 | }, 154 | vertexShader: /*glsl*/ ` 155 | varying vec2 vUv; 156 | void main() { 157 | vUv = uv; 158 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 159 | } 160 | `, 161 | fragmentShader: /*glsl*/ ` 162 | varying vec2 vUv; 163 | uniform highp sampler2D tDiffuse; 164 | highp float linearize_depth(highp float d, highp float zNear,highp float zFar) 165 | { 166 | highp float z_n = 2.0 * d - 1.0; 167 | return 2.0 * zNear * zFar / (zFar + zNear - z_n * (zFar - zNear)); 168 | } 169 | void main() { 170 | gl_FragColor = vec4(texture2D(tDiffuse, vUv).x, 0.0, 0.0, 1.0); 171 | } 172 | ` 173 | } 174 | const translucentHBlurQuad = new FullScreenQuad(new THREE.ShaderMaterial(translucentHBlur)); 175 | const translucentVBlurQuad = new FullScreenQuad(new THREE.ShaderMaterial(translucentVBlur)); 176 | const depthBlitQuad = new FullScreenQuad(new THREE.ShaderMaterial(depthBlit)); 177 | const thicknessMaterial = new THREE.ShaderMaterial({ 178 | depthWrite: false, 179 | depthTest: false, 180 | transparent: true, 181 | blending: THREE.AdditiveBlending, 182 | side: THREE.DoubleSide, 183 | uniforms: {}, 184 | vertexShader: /*glsl*/ ` 185 | varying vec3 vWorldPosition; 186 | void main() { 187 | vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz; 188 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 189 | } 190 | `, 191 | fragmentShader: /*glsl*/ ` 192 | varying vec3 vWorldPosition; 193 | void main() { 194 | gl_FragColor = vec4(vec3(distance(cameraPosition, vWorldPosition) * (gl_FrontFacing ? -1.0 : 1.0)), 1.0); 195 | } 196 | ` 197 | }); 198 | thicknessMaterial.forceSinglePass = true; 199 | 200 | const depthWriteOnlyMaterial = new THREE.MeshBasicMaterial({ 201 | colorWrite: false, 202 | depthWrite: true, 203 | side: THREE.DoubleSide, 204 | }); 205 | 206 | class MeshTranslucentMaterial extends THREE.MeshPhysicalMaterial { 207 | constructor(parameters) { 208 | super(); 209 | this.isMeshTranslucentMaterial = true; 210 | this.thicknessRenderTarget = null; 211 | this.thicknessRenderTargetBlur = null; 212 | this.thicknessRenderTargetDepth = null; 213 | this.renderTargetSize = new THREE.Vector2(); 214 | this.roughnessBlurScale = 16.0; 215 | this.resolutionScale = 0.5; 216 | this.scattering = 1.0; 217 | this.scatteringAbsorption = 1.0; 218 | this.internalRoughness = 0.5; 219 | this.setValues(parameters); 220 | /*this.onBeforeRender = (renderer, scene, camera, geometry, material, group) => { 221 | material.uniforms.uCameraPosition.value.copy(camera.position); 222 | };*/ 223 | } 224 | onBeforeRender(renderer, scene, camera, geometry, object, group) { 225 | renderer.getDrawingBufferSize(this.renderTargetSize); 226 | this.renderTargetSize.x = Math.floor(this.renderTargetSize.x * this.resolutionScale); 227 | this.renderTargetSize.y = Math.floor(this.renderTargetSize.y * this.resolutionScale); 228 | this.updateRenderTargets(); 229 | this.updateInternalUniforms(); 230 | const originClearAlpha = renderer.getClearAlpha(); 231 | const originAutoClear = renderer.autoClear; 232 | const originRenderTarget = renderer.getRenderTarget(); 233 | renderer.setRenderTarget(this.thicknessRenderTarget); 234 | renderer.setClearAlpha(0.0); 235 | renderer.clear(); 236 | renderer.autoClear = false; 237 | object.material = thicknessMaterial; 238 | renderer.render(object, camera); 239 | object.material = depthWriteOnlyMaterial; 240 | renderer.render(object, camera); 241 | renderer.setClearAlpha(originClearAlpha); 242 | object.material = this; 243 | renderer.autoClear = originAutoClear; 244 | renderer.setRenderTarget(this.thicknessRenderTargetDepth); 245 | renderer.clear(); 246 | depthBlitQuad.material.uniforms.tDiffuse.value = this.thicknessRenderTarget.depthTexture; 247 | depthBlitQuad.render(renderer); 248 | translucentHBlurQuad.material.uniforms.tDiffuse.value = this.thicknessRenderTarget.texture; 249 | translucentHBlurQuad.material.uniforms.tDepth.value = this.thicknessRenderTargetDepth.texture; 250 | translucentHBlurQuad.material.uniforms.resolution.value.copy(this.renderTargetSize); 251 | translucentHBlurQuad.material.uniforms.stride.value = this.roughnessBlurScale * 8.0 * this.roughness; 252 | translucentHBlurQuad.material.uniforms.near.value = camera.near; 253 | translucentHBlurQuad.material.uniforms.far.value = camera.far; 254 | translucentHBlurQuad.material.uniforms.orthographic.value = camera.isOrthographicCamera; 255 | translucentHBlurQuad.material.uniforms.logDepthBuffer.value = renderer.capabilities.logarithmicDepthBuffer; 256 | translucentVBlurQuad.material.uniforms.tDiffuse.value = this.thicknessRenderTargetBlur.texture; 257 | translucentVBlurQuad.material.uniforms.tDepth.value = this.thicknessRenderTargetDepth.texture; 258 | translucentVBlurQuad.material.uniforms.resolution.value.copy(this.renderTargetSize); 259 | translucentVBlurQuad.material.uniforms.stride.value = this.roughnessBlurScale * 8.0 * this.roughness; 260 | translucentVBlurQuad.material.uniforms.near.value = camera.near; 261 | translucentVBlurQuad.material.uniforms.far.value = camera.far; 262 | translucentVBlurQuad.material.uniforms.orthographic.value = camera.isOrthographicCamera; 263 | translucentVBlurQuad.material.uniforms.logDepthBuffer.value = renderer.capabilities.logarithmicDepthBuffer; 264 | renderer.setRenderTarget(this.thicknessRenderTargetBlur); 265 | translucentHBlurQuad.render(renderer); 266 | renderer.setRenderTarget(this.thicknessRenderTarget); 267 | translucentVBlurQuad.render(renderer); 268 | translucentHBlurQuad.material.uniforms.stride.value = this.roughnessBlurScale * 1.0 * this.roughness; 269 | translucentVBlurQuad.material.uniforms.stride.value = this.roughnessBlurScale * 1.0 * this.roughness; 270 | renderer.setRenderTarget(this.thicknessRenderTargetBlur); 271 | translucentHBlurQuad.render(renderer); 272 | renderer.setRenderTarget(this.thicknessRenderTarget); 273 | translucentVBlurQuad.render(renderer); 274 | renderer.setRenderTarget(originRenderTarget); 275 | } 276 | updateInternalUniforms() { 277 | if (!this._internalShader) return; 278 | if (this.thicknessRenderTarget) { 279 | this._internalShader.uniforms.thicknessTexture.value = this.thicknessRenderTarget.texture; 280 | } 281 | this._internalShader.uniforms.scattering.value = this.scattering; 282 | this._internalShader.uniforms.internalRoughness.value = this.internalRoughness; 283 | this._internalShader.uniforms.scatteringAbsorption.value = this.scatteringAbsorption; 284 | } 285 | onBeforeCompile(shader) { 286 | this._internalShader = shader; 287 | shader.uniforms.thicknessTexture = { value: this.thicknessRenderTarget.texture }; 288 | shader.uniforms.scattering = { value: this.scattering }; 289 | shader.uniforms.internalRoughness = { value: this.internalRoughness }; 290 | shader.uniforms.scatteringAbsorption = { value: this.scatteringAbsorption }; 291 | shader.fragmentShader = "uniform sampler2D thicknessTexture;\nuniform mat4 projectionMatrix;\nuniform float attenuationDistance;\nuniform float scattering;\nuniform float internalRoughness;\nuniform float scatteringAbsorption;\n" + shader.fragmentShader.replace( 292 | "#include ", 293 | /*glsl*/ 294 | ` 295 | uniform float transmission; 296 | uniform float thickness; 297 | uniform vec3 attenuationColor; 298 | #ifdef USE_TRANSMISSIONMAP 299 | uniform sampler2D transmissionMap; 300 | #endif 301 | #ifdef USE_THICKNESSMAP 302 | uniform sampler2D thicknessMap; 303 | #endif 304 | uniform vec2 transmissionSamplerSize; 305 | uniform sampler2D transmissionSamplerMap; 306 | uniform mat4 modelMatrix; 307 | varying vec3 vWorldPosition; 308 | float w0( float a ) { 309 | 310 | return ( 1.0 / 6.0 ) * ( a * ( a * ( - a + 3.0 ) - 3.0 ) + 1.0 ); 311 | 312 | } 313 | 314 | float w1( float a ) { 315 | 316 | return ( 1.0 / 6.0 ) * ( a * a * ( 3.0 * a - 6.0 ) + 4.0 ); 317 | 318 | } 319 | 320 | float w2( float a ){ 321 | 322 | return ( 1.0 / 6.0 ) * ( a * ( a * ( - 3.0 * a + 3.0 ) + 3.0 ) + 1.0 ); 323 | 324 | } 325 | 326 | float w3( float a ) { 327 | 328 | return ( 1.0 / 6.0 ) * ( a * a * a ); 329 | 330 | } 331 | 332 | // g0 and g1 are the two amplitude functions 333 | float g0( float a ) { 334 | 335 | return w0( a ) + w1( a ); 336 | 337 | } 338 | 339 | float g1( float a ) { 340 | 341 | return w2( a ) + w3( a ); 342 | 343 | } 344 | 345 | // h0 and h1 are the two offset functions 346 | float h0( float a ) { 347 | 348 | return - 1.0 + w1( a ) / ( w0( a ) + w1( a ) ); 349 | 350 | } 351 | 352 | float h1( float a ) { 353 | 354 | return 1.0 + w3( a ) / ( w2( a ) + w3( a ) ); 355 | 356 | } 357 | 358 | vec4 bicubic( sampler2D tex, vec2 uv, vec4 texelSize, float lod ) { 359 | 360 | uv = uv * texelSize.zw + 0.5; 361 | 362 | vec2 iuv = floor( uv ); 363 | vec2 fuv = fract( uv ); 364 | 365 | float g0x = g0( fuv.x ); 366 | float g1x = g1( fuv.x ); 367 | float h0x = h0( fuv.x ); 368 | float h1x = h1( fuv.x ); 369 | float h0y = h0( fuv.y ); 370 | float h1y = h1( fuv.y ); 371 | 372 | vec2 p0 = ( vec2( iuv.x + h0x, iuv.y + h0y ) - 0.5 ) * texelSize.xy; 373 | vec2 p1 = ( vec2( iuv.x + h1x, iuv.y + h0y ) - 0.5 ) * texelSize.xy; 374 | vec2 p2 = ( vec2( iuv.x + h0x, iuv.y + h1y ) - 0.5 ) * texelSize.xy; 375 | vec2 p3 = ( vec2( iuv.x + h1x, iuv.y + h1y ) - 0.5 ) * texelSize.xy; 376 | 377 | return g0( fuv.y ) * ( g0x * textureLod( tex, p0, lod ) + g1x * textureLod( tex, p1, lod ) ) + 378 | g1( fuv.y ) * ( g0x * textureLod( tex, p2, lod ) + g1x * textureLod( tex, p3, lod ) ); 379 | 380 | } 381 | 382 | vec4 textureBicubic( sampler2D sampler, vec2 uv, float lod ) { 383 | 384 | vec2 fLodSize = vec2( textureSize( sampler, int( lod ) ) ); 385 | vec2 cLodSize = vec2( textureSize( sampler, int( lod + 1.0 ) ) ); 386 | vec2 fLodSizeInv = 1.0 / fLodSize; 387 | vec2 cLodSizeInv = 1.0 / cLodSize; 388 | vec4 fSample = bicubic( sampler, uv, vec4( fLodSizeInv, fLodSize ), floor( lod ) ); 389 | vec4 cSample = bicubic( sampler, uv, vec4( cLodSizeInv, cLodSize ), ceil( lod ) ); 390 | return mix( fSample, cSample, fract( lod ) ); 391 | 392 | } 393 | 394 | vec3 getVolumeTransmissionRay(const in vec3 n, 395 | const in vec3 v, 396 | const in float thickness, 397 | const in float ior, 398 | const in mat4 modelMatrix) { 399 | // Direction of refracted light. 400 | vec3 refractionVector = refract(-v, normalize(n), 1.0 / ior); 401 | // Compute rotation-independant scaling of the model matrix. 402 | vec3 modelScale; 403 | modelScale.x = length(vec3(modelMatrix[0].xyz)); 404 | modelScale.y = length(vec3(modelMatrix[1].xyz)); 405 | modelScale.z = length(vec3(modelMatrix[2].xyz)); 406 | // The thickness is specified in local space. 407 | return normalize(refractionVector) * thickness * modelScale; 408 | } 409 | float applyIorToRoughness(const in float roughness, 410 | const in float ior) { 411 | // Scale roughness with IOR so that an IOR of 1.0 results in no microfacet refraction and 412 | // an IOR of 1.5 results in the default amount of microfacet refraction. 413 | return roughness * clamp(ior * 2.0 - 2.0, 0.0, 1.0); 414 | } 415 | vec4 getTransmissionSample(const in vec2 fragCoord, 416 | const in float roughness, 417 | const in float ior) { 418 | float lod = log2(transmissionSamplerSize.x) * applyIorToRoughness(roughness, ior); 419 | return textureBicubic(transmissionSamplerMap, fragCoord.xy, lod); 420 | } 421 | vec3 applyVolumeAttenuation(const in vec3 radiance, 422 | const in float transmissionDistance, 423 | const in vec3 attenuationColor, 424 | const in float attenuationDistance) { 425 | if (isinf(attenuationDistance)) { 426 | // Attenuation distance is +∞, i.e. the transmitted color is not attenuated at all. 427 | return radiance; 428 | } else { 429 | // Compute light attenuation using Beer's law. 430 | vec3 attenuationCoefficient = -log(attenuationColor) / attenuationDistance; 431 | vec3 transmittance = exp(-attenuationCoefficient * transmissionDistance); // Beer's law 432 | return transmittance * radiance; 433 | } 434 | } 435 | vec4 getIBLVolumeRefraction(const in vec3 n, 436 | const in vec3 v, 437 | const in float roughness, 438 | const in vec3 diffuseColor, 439 | const in vec3 specularColor, 440 | const in float specularF90, 441 | const in vec3 position, 442 | const in mat4 modelMatrix, 443 | const in mat4 viewMatrix, 444 | const in mat4 projMatrix, 445 | const in float ior, 446 | const in float thickness, 447 | const in vec3 attenuationColor, 448 | const in float attenuationDistance) { 449 | vec4 thicknessPos = projMatrix * viewMatrix * vec4(vWorldPosition, 1.0); 450 | vec2 thicknessCoords = thicknessPos.xy / thicknessPos.w; 451 | thicknessCoords += 1.0; 452 | thicknessCoords /= 2.0; 453 | float viewRayDepth = texture2D(thicknessTexture, thicknessCoords).x; 454 | vec3 transmissionRay = getVolumeTransmissionRay(n, v, viewRayDepth + thickness, ior, modelMatrix); 455 | vec3 refractedRayExit = position + transmissionRay; 456 | // Project refracted vector on the framebuffer, while mapping to normalized device coordinates. 457 | vec4 ndcPos = projMatrix * viewMatrix * vec4(refractedRayExit, 1.0); 458 | vec2 refractionCoords = ndcPos.xy / ndcPos.w; 459 | refractionCoords += 1.0; 460 | refractionCoords /= 2.0; 461 | // Sample framebuffer to get pixel the refracted ray hits. 462 | vec4 transmittedLight = getTransmissionSample(refractionCoords, roughness, ior); 463 | vec3 attenuatedColor = applyVolumeAttenuation(transmittedLight.rgb, viewRayDepth, attenuationColor, attenuationDistance); 464 | // Get the specular component. 465 | vec3 F = EnvironmentBRDF(n, v, specularColor, specularF90, roughness); 466 | return vec4((1.0 - F) * attenuatedColor * diffuseColor, transmittedLight.a); 467 | } 468 | ` 469 | ).replace("#include ", 470 | ` 471 | struct PhysicalMaterial { 472 | 473 | vec3 diffuseColor; 474 | float roughness; 475 | vec3 specularColor; 476 | float specularF90; 477 | 478 | #ifdef USE_CLEARCOAT 479 | float clearcoat; 480 | float clearcoatRoughness; 481 | vec3 clearcoatF0; 482 | float clearcoatF90; 483 | #endif 484 | 485 | #ifdef USE_IRIDESCENCE 486 | float iridescence; 487 | float iridescenceIOR; 488 | float iridescenceThickness; 489 | vec3 iridescenceFresnel; 490 | vec3 iridescenceF0; 491 | #endif 492 | 493 | #ifdef USE_SHEEN 494 | vec3 sheenColor; 495 | float sheenRoughness; 496 | #endif 497 | 498 | #ifdef IOR 499 | float ior; 500 | #endif 501 | 502 | #ifdef USE_TRANSMISSION 503 | float transmission; 504 | float transmissionAlpha; 505 | float thickness; 506 | float attenuationDistance; 507 | vec3 attenuationColor; 508 | #endif 509 | 510 | #ifdef USE_ANISOTROPY 511 | float anisotropy; 512 | float alphaT; 513 | vec3 anisotropyT; 514 | vec3 anisotropyB; 515 | #endif 516 | 517 | }; 518 | 519 | // temporary 520 | vec3 clearcoatSpecular = vec3( 0.0 ); 521 | vec3 sheenSpecular = vec3( 0.0 ); 522 | 523 | vec3 Schlick_to_F0( const in vec3 f, const in float f90, const in float dotVH ) { 524 | float x = clamp( 1.0 - dotVH, 0.0, 1.0 ); 525 | float x2 = x * x; 526 | float x5 = clamp( x * x2 * x2, 0.0, 0.9999 ); 527 | 528 | return ( f - vec3( f90 ) * x5 ) / ( 1.0 - x5 ); 529 | } 530 | 531 | // Moving Frostbite to Physically Based Rendering 3.0 - page 12, listing 2 532 | // https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf 533 | float V_GGX_SmithCorrelated( const in float alpha, const in float dotNL, const in float dotNV ) { 534 | 535 | float a2 = pow2( alpha ); 536 | 537 | float gv = dotNL * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNV ) ); 538 | float gl = dotNV * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNL ) ); 539 | 540 | return 0.5 / max( gv + gl, EPSILON ); 541 | 542 | } 543 | 544 | // Microfacet Models for Refraction through Rough Surfaces - equation (33) 545 | // http://graphicrants.blogspot.com/2013/08/specular-brdf-reference.html 546 | // alpha is "roughness squared" in Disney’s reparameterization 547 | float D_GGX( const in float alpha, const in float dotNH ) { 548 | 549 | float a2 = pow2( alpha ); 550 | 551 | float denom = pow2( dotNH ) * ( a2 - 1.0 ) + 1.0; // avoid alpha = 0 with dotNH = 1 552 | 553 | return RECIPROCAL_PI * a2 / pow2( denom ); 554 | 555 | } 556 | 557 | // https://google.github.io/filament/Filament.md.html#materialsystem/anisotropicmodel/anisotropicspecularbrdf 558 | #ifdef USE_ANISOTROPY 559 | 560 | float V_GGX_SmithCorrelated_Anisotropic( const in float alphaT, const in float alphaB, const in float dotTV, const in float dotBV, const in float dotTL, const in float dotBL, const in float dotNV, const in float dotNL ) { 561 | 562 | float gv = dotNL * length( vec3( alphaT * dotTV, alphaB * dotBV, dotNV ) ); 563 | float gl = dotNV * length( vec3( alphaT * dotTL, alphaB * dotBL, dotNL ) ); 564 | float v = 0.5 / ( gv + gl ); 565 | 566 | return saturate(v); 567 | 568 | } 569 | 570 | float D_GGX_Anisotropic( const in float alphaT, const in float alphaB, const in float dotNH, const in float dotTH, const in float dotBH ) { 571 | 572 | float a2 = alphaT * alphaB; 573 | highp vec3 v = vec3( alphaB * dotTH, alphaT * dotBH, a2 * dotNH ); 574 | highp float v2 = dot( v, v ); 575 | float w2 = a2 / v2; 576 | 577 | return RECIPROCAL_PI * a2 * pow2 ( w2 ); 578 | 579 | } 580 | 581 | #endif 582 | 583 | #ifdef USE_CLEARCOAT 584 | 585 | // GGX Distribution, Schlick Fresnel, GGX_SmithCorrelated Visibility 586 | vec3 BRDF_GGX_Clearcoat( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material) { 587 | 588 | vec3 f0 = material.clearcoatF0; 589 | float f90 = material.clearcoatF90; 590 | float roughness = material.clearcoatRoughness; 591 | 592 | float alpha = pow2( roughness ); // UE4's roughness 593 | 594 | vec3 halfDir = normalize( lightDir + viewDir ); 595 | 596 | float dotNL = saturate( dot( normal, lightDir ) ); 597 | float dotNV = saturate( dot( normal, viewDir ) ); 598 | float dotNH = saturate( dot( normal, halfDir ) ); 599 | float dotVH = saturate( dot( viewDir, halfDir ) ); 600 | 601 | vec3 F = F_Schlick( f0, f90, dotVH ); 602 | 603 | float V = V_GGX_SmithCorrelated( alpha, dotNL, dotNV ); 604 | 605 | float D = D_GGX( alpha, dotNH ); 606 | 607 | return F * ( V * D ); 608 | 609 | } 610 | 611 | #endif 612 | 613 | vec3 BRDF_GGX( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material ) { 614 | 615 | vec3 f0 = material.specularColor; 616 | float f90 = material.specularF90; 617 | float roughness = material.roughness; 618 | 619 | float alpha = pow2( roughness ); // UE4's roughness 620 | 621 | vec3 halfDir = normalize( lightDir + viewDir ); 622 | 623 | float dotNL = saturate( dot( normal, lightDir ) ); 624 | float dotNV = saturate( dot( normal, viewDir ) ); 625 | float dotNH = saturate( dot( normal, halfDir ) ); 626 | float dotVH = saturate( dot( viewDir, halfDir ) ); 627 | 628 | vec3 F = F_Schlick( f0, f90, dotVH ); 629 | 630 | #ifdef USE_IRIDESCENCE 631 | 632 | F = mix( F, material.iridescenceFresnel, material.iridescence ); 633 | 634 | #endif 635 | 636 | #ifdef USE_ANISOTROPY 637 | 638 | float dotTL = dot( material.anisotropyT, lightDir ); 639 | float dotTV = dot( material.anisotropyT, viewDir ); 640 | float dotTH = dot( material.anisotropyT, halfDir ); 641 | float dotBL = dot( material.anisotropyB, lightDir ); 642 | float dotBV = dot( material.anisotropyB, viewDir ); 643 | float dotBH = dot( material.anisotropyB, halfDir ); 644 | 645 | float V = V_GGX_SmithCorrelated_Anisotropic( material.alphaT, alpha, dotTV, dotBV, dotTL, dotBL, dotNV, dotNL ); 646 | 647 | float D = D_GGX_Anisotropic( material.alphaT, alpha, dotNH, dotTH, dotBH ); 648 | 649 | #else 650 | 651 | float V = V_GGX_SmithCorrelated( alpha, dotNL, dotNV ); 652 | 653 | float D = D_GGX( alpha, dotNH ); 654 | 655 | #endif 656 | 657 | return F * ( V * D ); 658 | 659 | } 660 | 661 | // Rect Area Light 662 | 663 | // Real-Time Polygonal-Light Shading with Linearly Transformed Cosines 664 | // by Eric Heitz, Jonathan Dupuy, Stephen Hill and David Neubelt 665 | // code: https://github.com/selfshadow/ltc_code/ 666 | 667 | vec2 LTC_Uv( const in vec3 N, const in vec3 V, const in float roughness ) { 668 | 669 | const float LUT_SIZE = 64.0; 670 | const float LUT_SCALE = ( LUT_SIZE - 1.0 ) / LUT_SIZE; 671 | const float LUT_BIAS = 0.5 / LUT_SIZE; 672 | 673 | float dotNV = saturate( dot( N, V ) ); 674 | 675 | // texture parameterized by sqrt( GGX alpha ) and sqrt( 1 - cos( theta ) ) 676 | vec2 uv = vec2( roughness, sqrt( 1.0 - dotNV ) ); 677 | 678 | uv = uv * LUT_SCALE + LUT_BIAS; 679 | 680 | return uv; 681 | 682 | } 683 | 684 | float LTC_ClippedSphereFormFactor( const in vec3 f ) { 685 | 686 | // Real-Time Area Lighting: a Journey from Research to Production (p.102) 687 | // An approximation of the form factor of a horizon-clipped rectangle. 688 | 689 | float l = length( f ); 690 | 691 | return max( ( l * l + f.z ) / ( l + 1.0 ), 0.0 ); 692 | 693 | } 694 | 695 | vec3 LTC_EdgeVectorFormFactor( const in vec3 v1, const in vec3 v2 ) { 696 | 697 | float x = dot( v1, v2 ); 698 | 699 | float y = abs( x ); 700 | 701 | // rational polynomial approximation to theta / sin( theta ) / 2PI 702 | float a = 0.8543985 + ( 0.4965155 + 0.0145206 * y ) * y; 703 | float b = 3.4175940 + ( 4.1616724 + y ) * y; 704 | float v = a / b; 705 | 706 | float theta_sintheta = ( x > 0.0 ) ? v : 0.5 * inversesqrt( max( 1.0 - x * x, 1e-7 ) ) - v; 707 | 708 | return cross( v1, v2 ) * theta_sintheta; 709 | 710 | } 711 | 712 | vec3 LTC_Evaluate( const in vec3 N, const in vec3 V, const in vec3 P, const in mat3 mInv, const in vec3 rectCoords[ 4 ] ) { 713 | 714 | // bail if point is on back side of plane of light 715 | // assumes ccw winding order of light vertices 716 | vec3 v1 = rectCoords[ 1 ] - rectCoords[ 0 ]; 717 | vec3 v2 = rectCoords[ 3 ] - rectCoords[ 0 ]; 718 | vec3 lightNormal = cross( v1, v2 ); 719 | 720 | if( dot( lightNormal, P - rectCoords[ 0 ] ) < 0.0 ) return vec3( 0.0 ); 721 | 722 | // construct orthonormal basis around N 723 | vec3 T1, T2; 724 | T1 = normalize( V - N * dot( V, N ) ); 725 | T2 = - cross( N, T1 ); // negated from paper; possibly due to a different handedness of world coordinate system 726 | 727 | // compute transform 728 | mat3 mat = mInv * transposeMat3( mat3( T1, T2, N ) ); 729 | 730 | // transform rect 731 | vec3 coords[ 4 ]; 732 | coords[ 0 ] = mat * ( rectCoords[ 0 ] - P ); 733 | coords[ 1 ] = mat * ( rectCoords[ 1 ] - P ); 734 | coords[ 2 ] = mat * ( rectCoords[ 2 ] - P ); 735 | coords[ 3 ] = mat * ( rectCoords[ 3 ] - P ); 736 | 737 | // project rect onto sphere 738 | coords[ 0 ] = normalize( coords[ 0 ] ); 739 | coords[ 1 ] = normalize( coords[ 1 ] ); 740 | coords[ 2 ] = normalize( coords[ 2 ] ); 741 | coords[ 3 ] = normalize( coords[ 3 ] ); 742 | 743 | // calculate vector form factor 744 | vec3 vectorFormFactor = vec3( 0.0 ); 745 | vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 0 ], coords[ 1 ] ); 746 | vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 1 ], coords[ 2 ] ); 747 | vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 2 ], coords[ 3 ] ); 748 | vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 3 ], coords[ 0 ] ); 749 | 750 | // adjust for horizon clipping 751 | float result = LTC_ClippedSphereFormFactor( vectorFormFactor ); 752 | 753 | /* 754 | // alternate method of adjusting for horizon clipping (see referece) 755 | // refactoring required 756 | float len = length( vectorFormFactor ); 757 | float z = vectorFormFactor.z / len; 758 | 759 | const float LUT_SIZE = 64.0; 760 | const float LUT_SCALE = ( LUT_SIZE - 1.0 ) / LUT_SIZE; 761 | const float LUT_BIAS = 0.5 / LUT_SIZE; 762 | 763 | // tabulated horizon-clipped sphere, apparently... 764 | vec2 uv = vec2( z * 0.5 + 0.5, len ); 765 | uv = uv * LUT_SCALE + LUT_BIAS; 766 | 767 | float scale = texture2D( ltc_2, uv ).w; 768 | 769 | float result = len * scale; 770 | */ 771 | 772 | return vec3( result ); 773 | 774 | } 775 | 776 | // End Rect Area Light 777 | 778 | #if defined( USE_SHEEN ) 779 | 780 | // https://github.com/google/filament/blob/master/shaders/src/brdf.fs 781 | float D_Charlie( float roughness, float dotNH ) { 782 | 783 | float alpha = pow2( roughness ); 784 | 785 | // Estevez and Kulla 2017, "Production Friendly Microfacet Sheen BRDF" 786 | float invAlpha = 1.0 / alpha; 787 | float cos2h = dotNH * dotNH; 788 | float sin2h = max( 1.0 - cos2h, 0.0078125 ); // 2^(-14/2), so sin2h^2 > 0 in fp16 789 | 790 | return ( 2.0 + invAlpha ) * pow( sin2h, invAlpha * 0.5 ) / ( 2.0 * PI ); 791 | 792 | } 793 | 794 | // https://github.com/google/filament/blob/master/shaders/src/brdf.fs 795 | float V_Neubelt( float dotNV, float dotNL ) { 796 | 797 | // Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886" 798 | return saturate( 1.0 / ( 4.0 * ( dotNL + dotNV - dotNL * dotNV ) ) ); 799 | 800 | } 801 | 802 | vec3 BRDF_Sheen( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, vec3 sheenColor, const in float sheenRoughness ) { 803 | 804 | vec3 halfDir = normalize( lightDir + viewDir ); 805 | 806 | float dotNL = saturate( dot( normal, lightDir ) ); 807 | float dotNV = saturate( dot( normal, viewDir ) ); 808 | float dotNH = saturate( dot( normal, halfDir ) ); 809 | 810 | float D = D_Charlie( sheenRoughness, dotNH ); 811 | float V = V_Neubelt( dotNV, dotNL ); 812 | 813 | return sheenColor * ( D * V ); 814 | 815 | } 816 | 817 | #endif 818 | 819 | // This is a curve-fit approxmation to the "Charlie sheen" BRDF integrated over the hemisphere from 820 | // Estevez and Kulla 2017, "Production Friendly Microfacet Sheen BRDF". The analysis can be found 821 | // in the Sheen section of https://drive.google.com/file/d/1T0D1VSyR4AllqIJTQAraEIzjlb5h4FKH/view?usp=sharing 822 | float IBLSheenBRDF( const in vec3 normal, const in vec3 viewDir, const in float roughness ) { 823 | 824 | float dotNV = saturate( dot( normal, viewDir ) ); 825 | 826 | float r2 = roughness * roughness; 827 | 828 | float a = roughness < 0.25 ? -339.2 * r2 + 161.4 * roughness - 25.9 : -8.48 * r2 + 14.3 * roughness - 9.95; 829 | 830 | float b = roughness < 0.25 ? 44.0 * r2 - 23.7 * roughness + 3.26 : 1.97 * r2 - 3.27 * roughness + 0.72; 831 | 832 | float DG = exp( a * dotNV + b ) + ( roughness < 0.25 ? 0.0 : 0.1 * ( roughness - 0.25 ) ); 833 | 834 | return saturate( DG * RECIPROCAL_PI ); 835 | 836 | } 837 | 838 | // Analytical approximation of the DFG LUT, one half of the 839 | // split-sum approximation used in indirect specular lighting. 840 | // via 'environmentBRDF' from "Physically Based Shading on Mobile" 841 | // https://www.unrealengine.com/blog/physically-based-shading-on-mobile 842 | vec2 DFGApprox( const in vec3 normal, const in vec3 viewDir, const in float roughness ) { 843 | 844 | float dotNV = saturate( dot( normal, viewDir ) ); 845 | 846 | const vec4 c0 = vec4( - 1, - 0.0275, - 0.572, 0.022 ); 847 | 848 | const vec4 c1 = vec4( 1, 0.0425, 1.04, - 0.04 ); 849 | 850 | vec4 r = roughness * c0 + c1; 851 | 852 | float a004 = min( r.x * r.x, exp2( - 9.28 * dotNV ) ) * r.x + r.y; 853 | 854 | vec2 fab = vec2( - 1.04, 1.04 ) * a004 + r.zw; 855 | 856 | return fab; 857 | 858 | } 859 | 860 | vec3 EnvironmentBRDF( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness ) { 861 | 862 | vec2 fab = DFGApprox( normal, viewDir, roughness ); 863 | 864 | return specularColor * fab.x + specularF90 * fab.y; 865 | 866 | } 867 | 868 | // Fdez-Agüera's "Multiple-Scattering Microfacet Model for Real-Time Image Based Lighting" 869 | // Approximates multiscattering in order to preserve energy. 870 | // http://www.jcgt.org/published/0008/01/03/ 871 | #ifdef USE_IRIDESCENCE 872 | void computeMultiscatteringIridescence( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float iridescence, const in vec3 iridescenceF0, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) { 873 | #else 874 | void computeMultiscattering( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) { 875 | #endif 876 | 877 | vec2 fab = DFGApprox( normal, viewDir, roughness ); 878 | 879 | #ifdef USE_IRIDESCENCE 880 | 881 | vec3 Fr = mix( specularColor, iridescenceF0, iridescence ); 882 | 883 | #else 884 | 885 | vec3 Fr = specularColor; 886 | 887 | #endif 888 | 889 | vec3 FssEss = Fr * fab.x + specularF90 * fab.y; 890 | 891 | float Ess = fab.x + fab.y; 892 | float Ems = 1.0 - Ess; 893 | 894 | vec3 Favg = Fr + ( 1.0 - Fr ) * 0.047619; // 1/21 895 | vec3 Fms = FssEss * Favg / ( 1.0 - Ems * Favg ); 896 | 897 | singleScatter += FssEss; 898 | multiScatter += Fms * Ems; 899 | 900 | } 901 | 902 | #if NUM_RECT_AREA_LIGHTS > 0 903 | 904 | void RE_Direct_RectArea_Physical( const in RectAreaLight rectAreaLight, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) { 905 | 906 | vec3 normal = geometry.normal; 907 | vec3 viewDir = geometry.viewDir; 908 | vec3 position = geometry.position; 909 | vec3 lightPos = rectAreaLight.position; 910 | vec3 halfWidth = rectAreaLight.halfWidth; 911 | vec3 halfHeight = rectAreaLight.halfHeight; 912 | vec3 lightColor = rectAreaLight.color; 913 | float roughness = material.roughness; 914 | 915 | vec3 rectCoords[ 4 ]; 916 | rectCoords[ 0 ] = lightPos + halfWidth - halfHeight; // counterclockwise; light shines in local neg z direction 917 | rectCoords[ 1 ] = lightPos - halfWidth - halfHeight; 918 | rectCoords[ 2 ] = lightPos - halfWidth + halfHeight; 919 | rectCoords[ 3 ] = lightPos + halfWidth + halfHeight; 920 | 921 | vec2 uv = LTC_Uv( normal, viewDir, roughness ); 922 | 923 | vec4 t1 = texture2D( ltc_1, uv ); 924 | vec4 t2 = texture2D( ltc_2, uv ); 925 | 926 | mat3 mInv = mat3( 927 | vec3( t1.x, 0, t1.y ), 928 | vec3( 0, 1, 0 ), 929 | vec3( t1.z, 0, t1.w ) 930 | ); 931 | 932 | // LTC Fresnel Approximation by Stephen Hill 933 | // http://blog.selfshadow.com/publications/s2016-advances/s2016_ltc_fresnel.pdf 934 | vec3 fresnel = ( material.specularColor * t2.x + ( vec3( 1.0 ) - material.specularColor ) * t2.y ); 935 | 936 | reflectedLight.directSpecular += lightColor * fresnel * LTC_Evaluate( normal, viewDir, position, mInv, rectCoords ); 937 | 938 | reflectedLight.directDiffuse += lightColor * material.diffuseColor * LTC_Evaluate( normal, viewDir, position, mat3( 1.0 ), rectCoords ); 939 | 940 | } 941 | 942 | #endif 943 | void RE_Direct_Physical( const in IncidentLight directLight, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) { 944 | 945 | float dotNL = saturate( dot( geometry.normal, directLight.direction ) ); 946 | 947 | vec3 irradiance = dotNL * directLight.color; 948 | 949 | #ifdef USE_CLEARCOAT 950 | 951 | float dotNLcc = saturate( dot( geometry.clearcoatNormal, directLight.direction ) ); 952 | 953 | vec3 ccIrradiance = dotNLcc * directLight.color; 954 | 955 | clearcoatSpecular += ccIrradiance * BRDF_GGX_Clearcoat( directLight.direction, geometry.viewDir, geometry.clearcoatNormal, material ); 956 | 957 | #endif 958 | 959 | #ifdef USE_SHEEN 960 | 961 | sheenSpecular += irradiance * BRDF_Sheen( directLight.direction, geometry.viewDir, geometry.normal, material.sheenColor, material.sheenRoughness ); 962 | 963 | #endif 964 | 965 | reflectedLight.directSpecular += irradiance * BRDF_GGX( directLight.direction, geometry.viewDir, geometry.normal, material ); 966 | // Calculate subsurface scattering 967 | vec4 position = projectionMatrix * vec4(geometry.position, 1.0); 968 | vec2 uv = position.xy / position.w; 969 | uv = uv * 0.5 + 0.5; 970 | float thickness = texture2D(thicknessTexture, uv).r; 971 | vec3 scatteringHalf = normalize(directLight.direction + (geometry.normal * internalRoughness)); 972 | float dotNLSubsurface = saturate( dot( geometry.viewDir, -scatteringHalf) ); 973 | float specPow = mix(256.0, mix(1.0, 256.0, pow(1.0 - internalRoughness, 5.185)), pow(material.roughness, 0.1)); 974 | vec3 subsurfaceIrradiance = scattering * pow(dotNLSubsurface, specPow) * BRDF_Lambert(directLight.color) * exp(-(1.0 / attenuationDistance) * thickness * (0.15 - 0.1 * internalRoughness) * scatteringAbsorption); 975 | reflectedLight.directSpecular += subsurfaceIrradiance; 976 | reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); 977 | } 978 | void RE_IndirectDiffuse_Physical( const in vec3 irradiance, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) { 979 | 980 | reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); 981 | 982 | } 983 | 984 | void RE_IndirectSpecular_Physical( const in vec3 radiance, const in vec3 irradiance, const in vec3 clearcoatRadiance, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight) { 985 | 986 | #ifdef USE_CLEARCOAT 987 | 988 | clearcoatSpecular += clearcoatRadiance * EnvironmentBRDF( geometry.clearcoatNormal, geometry.viewDir, material.clearcoatF0, material.clearcoatF90, material.clearcoatRoughness ); 989 | 990 | #endif 991 | 992 | #ifdef USE_SHEEN 993 | 994 | sheenSpecular += irradiance * material.sheenColor * IBLSheenBRDF( geometry.normal, geometry.viewDir, material.sheenRoughness ); 995 | 996 | #endif 997 | 998 | // Both indirect specular and indirect diffuse light accumulate here 999 | 1000 | vec3 singleScattering = vec3( 0.0 ); 1001 | vec3 multiScattering = vec3( 0.0 ); 1002 | vec3 cosineWeightedIrradiance = irradiance * RECIPROCAL_PI; 1003 | 1004 | #ifdef USE_IRIDESCENCE 1005 | 1006 | computeMultiscatteringIridescence( geometry.normal, geometry.viewDir, material.specularColor, material.specularF90, material.iridescence, material.iridescenceFresnel, material.roughness, singleScattering, multiScattering ); 1007 | 1008 | #else 1009 | 1010 | computeMultiscattering( geometry.normal, geometry.viewDir, material.specularColor, material.specularF90, material.roughness, singleScattering, multiScattering ); 1011 | 1012 | #endif 1013 | 1014 | vec3 totalScattering = singleScattering + multiScattering; 1015 | vec3 diffuse = material.diffuseColor * ( 1.0 - max( max( totalScattering.r, totalScattering.g ), totalScattering.b ) ); 1016 | 1017 | reflectedLight.indirectSpecular += radiance * singleScattering; 1018 | reflectedLight.indirectSpecular += multiScattering * cosineWeightedIrradiance; 1019 | 1020 | reflectedLight.indirectDiffuse += diffuse * cosineWeightedIrradiance; 1021 | 1022 | } 1023 | 1024 | #define RE_Direct RE_Direct_Physical 1025 | #define RE_Direct_RectArea RE_Direct_RectArea_Physical 1026 | #define RE_IndirectDiffuse RE_IndirectDiffuse_Physical 1027 | #define RE_IndirectSpecular RE_IndirectSpecular_Physical 1028 | 1029 | // ref: https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf 1030 | float computeSpecularOcclusion( const in float dotNV, const in float ambientOcclusion, const in float roughness ) { 1031 | 1032 | return saturate( pow( dotNV + ambientOcclusion, exp2( - 16.0 * roughness - 1.0 ) ) - 1.0 + ambientOcclusion ); 1033 | 1034 | } 1035 | `); 1036 | } 1037 | updateRenderTargets() { 1038 | if (this.thicknessRenderTarget === null) { 1039 | this.thicknessRenderTarget = new THREE.WebGLRenderTarget(this.renderTargetSize.x, this.renderTargetSize.y, { 1040 | minFilter: THREE.LinearFilter, 1041 | magFilter: THREE.LinearFilter, 1042 | type: THREE.FloatType, 1043 | }); 1044 | this.thicknessRenderTarget.depthTexture = new THREE.DepthTexture(this.thicknessRenderTarget, this.renderTargetSize.y, THREE.FloatType); 1045 | } 1046 | if (this.thicknessRenderTargetBlur === null) { 1047 | this.thicknessRenderTargetBlur = new THREE.WebGLRenderTarget(this.renderTargetSize.x, this.renderTargetSize.y, { 1048 | minFilter: THREE.LinearFilter, 1049 | magFilter: THREE.LinearFilter, 1050 | type: THREE.FloatType, 1051 | }); 1052 | } 1053 | if (this.thicknessRenderTargetDepth === null) { 1054 | this.thicknessRenderTargetDepth = new THREE.WebGLRenderTarget(this.renderTargetSize.x, this.renderTargetSize.y, { 1055 | minFilter: THREE.LinearFilter, 1056 | magFilter: THREE.LinearFilter, 1057 | type: THREE.FloatType, 1058 | }); 1059 | } 1060 | if (this.thicknessRenderTarget.width !== this.renderTargetSize.x || this.thicknessRenderTarget.height !== this.renderTargetSize.y) { 1061 | this.thicknessRenderTarget.setSize(this.renderTargetSize.x, this.renderTargetSize.y); 1062 | this.thicknessRenderTargetDepth.setSize(this.renderTargetSize.x, this.renderTargetSize.y); 1063 | this.thicknessRenderTargetBlur.setSize(this.renderTargetSize.x, this.renderTargetSize.y); 1064 | } 1065 | } 1066 | } 1067 | 1068 | export { MeshTranslucentMaterial }; -------------------------------------------------------------------------------- /dragon.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/N8python/subsurfaceScatteringMat/47c7bbc6a0e29cadff85b951a2c52e1e315fb32f/dragon.glb -------------------------------------------------------------------------------- /dragonthicc.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/N8python/subsurfaceScatteringMat/47c7bbc6a0e29cadff85b951a2c52e1e315fb32f/dragonthicc.glb -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Translucency 9 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.skypack.dev/three@0.152.0'; 2 | import { EffectComposer } from 'https://unpkg.com/three@0.152.0/examples/jsm/postprocessing/EffectComposer.js'; 3 | import { RenderPass } from 'https://unpkg.com/three@0.152.0/examples/jsm/postprocessing/RenderPass.js'; 4 | import { ShaderPass } from 'https://unpkg.com/three@0.152.0/examples/jsm/postprocessing/ShaderPass.js'; 5 | import { SMAAPass } from 'https://unpkg.com/three@0.152.0/examples/jsm/postprocessing/SMAAPass.js'; 6 | import { GammaCorrectionShader } from 'https://unpkg.com/three@0.152.0/examples/jsm/shaders/GammaCorrectionShader.js'; 7 | import { EffectShader } from "./EffectShader.js"; 8 | import { OrbitControls } from 'https://unpkg.com/three@0.152.0/examples/jsm/controls/OrbitControls.js'; 9 | import { OBJLoader } from "https://unpkg.com/three@0.152.0/examples/jsm/loaders/OBJLoader.js"; 10 | import { GLTFLoader } from "https://unpkg.com/three@0.152.0/examples/jsm/loaders/GLTFLoader.js"; 11 | import * as BufferGeometryUtils from "https://unpkg.com/three@0.152.0/examples/jsm/utils/BufferGeometryUtils.js"; 12 | import { FullScreenQuad } from "https://unpkg.com/three@0.152.0/examples/jsm/postprocessing/Pass.js"; 13 | import { GUI } from 'https://unpkg.com/three@0.152.0/examples/jsm/libs/lil-gui.module.min.js'; 14 | import { AssetManager } from './AssetManager.js'; 15 | import { MeshTranslucentMaterial } from './TranslucentMaterial.js'; 16 | import { Stats } from "./stats.js"; 17 | async function main() { 18 | // Setup basic renderer, controls, and profiler 19 | const clientWidth = window.innerWidth; 20 | const clientHeight = window.innerHeight; 21 | const scene = new THREE.Scene(); 22 | const camera = new THREE.PerspectiveCamera(75, clientWidth / clientHeight, 0.1, 1000); 23 | camera.position.set(50, 75, 50); 24 | const renderer = new THREE.WebGLRenderer({ antialias: true }); 25 | renderer.setSize(clientWidth, clientHeight); 26 | window.addEventListener('resize', () => { 27 | const clientWidth = window.innerWidth; 28 | const clientHeight = window.innerHeight; 29 | renderer.setSize(clientWidth, clientHeight); 30 | camera.aspect = clientWidth / clientHeight; 31 | camera.updateProjectionMatrix(); 32 | }); 33 | renderer.setPixelRatio(window.devicePixelRatio); 34 | renderer.outputEncoding = THREE.sRGBEncoding; 35 | document.body.appendChild(renderer.domElement); 36 | renderer.shadowMap.enabled = true; 37 | renderer.shadowMap.type = THREE.PCFSoftShadowMap; 38 | const controls = new OrbitControls(camera, renderer.domElement); 39 | controls.target.set(0, 25, 0); 40 | const stats = new Stats(); 41 | stats.showPanel(0); 42 | document.body.appendChild(stats.dom); 43 | // Setup scene 44 | // Skybox 45 | const environment = new THREE.CubeTextureLoader().load([ 46 | "skybox/Box_Right.bmp", 47 | "skybox/Box_Left.bmp", 48 | "skybox/Box_Top.bmp", 49 | "skybox/Box_Bottom.bmp", 50 | "skybox/Box_Front.bmp", 51 | "skybox/Box_Back.bmp" 52 | ]); 53 | environment.encoding = THREE.sRGBEncoding; 54 | scene.background = environment; 55 | const rSize = renderer.getDrawingBufferSize(new THREE.Vector2()); 56 | rSize.x = Math.floor(rSize.x / 2); 57 | rSize.y = Math.floor(rSize.y / 2); 58 | // Lighting 59 | const ambientLight = new THREE.AmbientLight(new THREE.Color(1.0, 1.0, 1.0), 0.25); 60 | scene.add(ambientLight); 61 | const directionalLight = new THREE.DirectionalLight(0xffffff, 0.35); 62 | directionalLight.position.set(152, 200, 50); 63 | // Shadows 64 | directionalLight.castShadow = true; 65 | directionalLight.shadow.mapSize.width = 1024; 66 | directionalLight.shadow.mapSize.height = 1024; 67 | directionalLight.shadow.camera.left = -75; 68 | directionalLight.shadow.camera.right = 75; 69 | directionalLight.shadow.camera.top = 75; 70 | directionalLight.shadow.camera.bottom = -75; 71 | directionalLight.shadow.camera.near = 0.1; 72 | directionalLight.shadow.camera.far = 500; 73 | directionalLight.shadow.bias = -0.001; 74 | directionalLight.shadow.blurSamples = 8; 75 | directionalLight.shadow.radius = 4; 76 | scene.add(directionalLight); 77 | // Add 6 point lights 78 | const colors = [ 79 | new THREE.Color(1.0, 0.0, 0.0), 80 | new THREE.Color(0.0, 1.0, 0.0), 81 | new THREE.Color(0.0, 0.0, 1.0), 82 | new THREE.Color(1.0, 1.0, 0.0), 83 | new THREE.Color(1.0, 0.0, 1.0), 84 | new THREE.Color(0.0, 1.0, 1.0) 85 | ]; 86 | const lights = []; 87 | for (let i = 0; i < 6; i++) { 88 | let color = colors[i % 6]; 89 | const pointLight = new THREE.PointLight(color, 0.25); 90 | pointLight.position.set(40 * Math.random() - 20, 5 + 10 * Math.random(), 40 * Math.random() - 20); 91 | scene.add(pointLight); 92 | pointLight.add(new THREE.Mesh(new THREE.SphereGeometry(1, 32, 32), new THREE.MeshBasicMaterial({ color: color }))); 93 | lights.push(pointLight); 94 | } 95 | // Objects 96 | const ground = new THREE.Mesh(new THREE.PlaneGeometry(100, 100).applyMatrix4(new THREE.Matrix4().makeRotationX(-Math.PI / 2)), new THREE.MeshStandardMaterial({ side: THREE.DoubleSide, roughness: 0, envMap: environment, color: new THREE.Color(0.5, 0.5, 0.5) })); 97 | ground.castShadow = true; 98 | ground.receiveShadow = true; 99 | scene.add(ground); 100 | let buddha = await new OBJLoader().loadAsync("https://raw.githubusercontent.com/alecjacobson/common-3d-test-models/master/data/happy.obj"); 101 | buddha.traverse((child) => { 102 | if (child.isMesh) { 103 | buddha = child; 104 | } 105 | }); 106 | buddha.geometry.deleteAttribute('normal'); 107 | buddha.geometry = BufferGeometryUtils.mergeVertices(buddha.geometry); 108 | buddha.geometry.computeVertexNormals(); 109 | buddha.updateMatrixWorld(); 110 | buddha.geometry.applyMatrix4(buddha.matrixWorld); 111 | buddha.geometry.center(); 112 | buddha.geometry.scale(100, 100, 100); 113 | const translucentMesh = new THREE.Mesh(buddha.geometry, new MeshTranslucentMaterial({ side: THREE.DoubleSide, envMap: environment, transmission: 1.0, roughness: 0.5, ior: 1.5, attenuationColor: new THREE.Color(0.9, 0.6, 0.3), attenuationDistance: 0.33, dithering: true, thickness: 2.0 })); 114 | translucentMesh.position.y = new THREE.Box3().setFromObject(translucentMesh, true).getSize(new THREE.Vector3()).y / 2; 115 | translucentMesh.castShadow = true; 116 | //translucentMesh.receiveShadow = true; 117 | scene.add(translucentMesh); 118 | 119 | const clock = new THREE.Clock(); 120 | const gui = new GUI(); 121 | const effectController = { 122 | roughness: 0.5, 123 | internalRoughness: 0.5, 124 | scatteringAbsorption: 1.0, 125 | scattering: 1.0, 126 | roughnessBlurScale: 16.0, 127 | resolutionScale: 0.5, 128 | attenuationDistance: 0.33, 129 | attenuationColor: [0.9, 0.6, 0.3], 130 | moveLights: false 131 | } 132 | gui.add(effectController, "roughness", 0.0, 1.0, 0.01).onChange((value) => { 133 | translucentMesh.material.roughness = value; 134 | }); 135 | gui.add(effectController, "internalRoughness", 0.0, 1.0, 0.01).onChange((value) => { 136 | translucentMesh.material.internalRoughness = value; 137 | }); 138 | gui.add(effectController, "scatteringAbsorption", 0.0, 1.0, 0.01).onChange((value) => { 139 | translucentMesh.material.scatteringAbsorption = value; 140 | }); 141 | gui.add(effectController, "scattering", 0.0, 1.0, 0.01).onChange((value) => { 142 | translucentMesh.material.scattering = value; 143 | }); 144 | gui.add(effectController, "roughnessBlurScale", 0.0, 32.0, 0.01).onChange((value) => { 145 | translucentMesh.material.roughnessBlurScale = value; 146 | }); 147 | gui.add(effectController, "resolutionScale", 0.25, 1.0, 0.01).onChange((value) => { 148 | translucentMesh.material.resolutionScale = value; 149 | }); 150 | gui.add(effectController, "attenuationDistance", 0.0, 1.0, 0.01).onChange((value) => { 151 | translucentMesh.material.attenuationDistance = value; 152 | }); 153 | gui.addColor(effectController, "attenuationColor").onChange((value) => { 154 | translucentMesh.material.attenuationColor = new THREE.Color(value[0], value[1], value[2]); 155 | }); 156 | gui.add(effectController, "moveLights"); 157 | 158 | 159 | 160 | 161 | function animate() { 162 | const delta = clock.getDelta(); 163 | if (effectController.moveLights) { 164 | for (let i = 0; i < lights.length; i++) { 165 | const light = lights[i]; 166 | const mag = Math.max(Math.hypot(light.position.x, light.position.z), 5); 167 | const angle = Math.atan2(light.position.z, light.position.x) + delta; 168 | light.position.x = mag * Math.cos(angle); 169 | light.position.z = mag * Math.sin(angle); 170 | } 171 | } 172 | renderer.render(scene, camera); 173 | controls.update(); 174 | stats.update(); 175 | requestAnimationFrame(animate); 176 | } 177 | requestAnimationFrame(animate); 178 | } 179 | main(); -------------------------------------------------------------------------------- /skybox/Box_Back.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/N8python/subsurfaceScatteringMat/47c7bbc6a0e29cadff85b951a2c52e1e315fb32f/skybox/Box_Back.bmp -------------------------------------------------------------------------------- /skybox/Box_Bottom.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/N8python/subsurfaceScatteringMat/47c7bbc6a0e29cadff85b951a2c52e1e315fb32f/skybox/Box_Bottom.bmp -------------------------------------------------------------------------------- /skybox/Box_Front.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/N8python/subsurfaceScatteringMat/47c7bbc6a0e29cadff85b951a2c52e1e315fb32f/skybox/Box_Front.bmp -------------------------------------------------------------------------------- /skybox/Box_Left.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/N8python/subsurfaceScatteringMat/47c7bbc6a0e29cadff85b951a2c52e1e315fb32f/skybox/Box_Left.bmp -------------------------------------------------------------------------------- /skybox/Box_Right.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/N8python/subsurfaceScatteringMat/47c7bbc6a0e29cadff85b951a2c52e1e315fb32f/skybox/Box_Right.bmp -------------------------------------------------------------------------------- /skybox/Box_Top.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/N8python/subsurfaceScatteringMat/47c7bbc6a0e29cadff85b951a2c52e1e315fb32f/skybox/Box_Top.bmp -------------------------------------------------------------------------------- /stats.js: -------------------------------------------------------------------------------- 1 | var Stats = function() { 2 | 3 | var mode = 0; 4 | 5 | var container = document.createElement('div'); 6 | container.style.cssText = 'position:fixed;top:0;left:0;cursor:pointer;opacity:0.9;z-index:10000'; 7 | container.addEventListener('click', function(event) { 8 | 9 | event.preventDefault(); 10 | showPanel(++mode % container.children.length); 11 | 12 | }, false); 13 | 14 | // 15 | 16 | function addPanel(panel) { 17 | 18 | container.appendChild(panel.dom); 19 | return panel; 20 | 21 | } 22 | 23 | function showPanel(id) { 24 | 25 | for (var i = 0; i < container.children.length; i++) { 26 | 27 | container.children[i].style.display = i === id ? 'block' : 'none'; 28 | 29 | } 30 | 31 | mode = id; 32 | 33 | } 34 | 35 | // 36 | 37 | var beginTime = (performance || Date).now(), 38 | prevTime = beginTime, 39 | frames = 0; 40 | 41 | var fpsPanel = addPanel(new Stats.Panel('FPS', '#0ff', '#002')); 42 | var msPanel = addPanel(new Stats.Panel('MS', '#0f0', '#020')); 43 | 44 | if (self.performance && self.performance.memory) { 45 | 46 | var memPanel = addPanel(new Stats.Panel('MB', '#f08', '#201')); 47 | 48 | } 49 | 50 | showPanel(0); 51 | 52 | return { 53 | 54 | REVISION: 16, 55 | 56 | dom: container, 57 | 58 | addPanel: addPanel, 59 | showPanel: showPanel, 60 | 61 | begin: function() { 62 | 63 | beginTime = (performance || Date).now(); 64 | 65 | }, 66 | 67 | end: function() { 68 | 69 | frames++; 70 | 71 | var time = (performance || Date).now(); 72 | 73 | msPanel.update(time - beginTime, 200); 74 | 75 | if (time >= prevTime + 1000) { 76 | 77 | fpsPanel.update((frames * 1000) / (time - prevTime), 100); 78 | 79 | prevTime = time; 80 | frames = 0; 81 | 82 | if (memPanel) { 83 | 84 | var memory = performance.memory; 85 | memPanel.update(memory.usedJSHeapSize / 1048576, memory.jsHeapSizeLimit / 1048576); 86 | 87 | } 88 | 89 | } 90 | 91 | return time; 92 | 93 | }, 94 | 95 | update: function() { 96 | 97 | beginTime = this.end(); 98 | 99 | }, 100 | 101 | // Backwards Compatibility 102 | 103 | domElement: container, 104 | setMode: showPanel 105 | 106 | }; 107 | 108 | }; 109 | 110 | Stats.Panel = function(name, fg, bg) { 111 | 112 | var min = Infinity, 113 | max = 0, 114 | round = Math.round; 115 | var PR = round(window.devicePixelRatio || 1); 116 | 117 | var WIDTH = 80 * PR, 118 | HEIGHT = 48 * PR, 119 | TEXT_X = 3 * PR, 120 | TEXT_Y = 2 * PR, 121 | GRAPH_X = 3 * PR, 122 | GRAPH_Y = 15 * PR, 123 | GRAPH_WIDTH = 74 * PR, 124 | GRAPH_HEIGHT = 30 * PR; 125 | 126 | var canvas = document.createElement('canvas'); 127 | canvas.width = WIDTH; 128 | canvas.height = HEIGHT; 129 | canvas.style.cssText = 'width:80px;height:48px'; 130 | 131 | var context = canvas.getContext('2d'); 132 | context.font = 'bold ' + (9 * PR) + 'px Helvetica,Arial,sans-serif'; 133 | context.textBaseline = 'top'; 134 | 135 | context.fillStyle = bg; 136 | context.fillRect(0, 0, WIDTH, HEIGHT); 137 | 138 | context.fillStyle = fg; 139 | context.fillText(name, TEXT_X, TEXT_Y); 140 | context.fillRect(GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT); 141 | 142 | context.fillStyle = bg; 143 | context.globalAlpha = 0.9; 144 | context.fillRect(GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT); 145 | 146 | return { 147 | 148 | dom: canvas, 149 | 150 | update: function(value, maxValue) { 151 | 152 | min = Math.min(min, value); 153 | max = Math.max(max, value); 154 | 155 | context.fillStyle = bg; 156 | context.globalAlpha = 1; 157 | context.fillRect(0, 0, WIDTH, GRAPH_Y); 158 | context.fillStyle = fg; 159 | context.fillText(round(value) + ' ' + name + ' (' + round(min) + '-' + round(max) + ')', TEXT_X, TEXT_Y); 160 | 161 | context.drawImage(canvas, GRAPH_X + PR, GRAPH_Y, GRAPH_WIDTH - PR, GRAPH_HEIGHT, GRAPH_X, GRAPH_Y, GRAPH_WIDTH - PR, GRAPH_HEIGHT); 162 | 163 | context.fillRect(GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, GRAPH_HEIGHT); 164 | 165 | context.fillStyle = bg; 166 | context.globalAlpha = 0.9; 167 | context.fillRect(GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, round((1 - (value / maxValue)) * GRAPH_HEIGHT)); 168 | 169 | } 170 | 171 | }; 172 | 173 | }; 174 | export { Stats }; --------------------------------------------------------------------------------