|
| 1 | +<!DOCTYPE html> |
| 2 | +<html lang="en"> |
| 3 | + <head> |
| 4 | + <title>three.js webgl2 - volume - cloud</title> |
| 5 | + <meta charset="utf-8"> |
| 6 | + <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"> |
| 7 | + <link type="text/css" rel="stylesheet" href="main.css"> |
| 8 | + </head> |
| 9 | + |
| 10 | + <body> |
| 11 | + <div id="info"> |
| 12 | + <a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgl2 - volume - cloud |
| 13 | + </div> |
| 14 | + |
| 15 | + <script type="module"> |
| 16 | + import * as THREE from '../build/three.module.js'; |
| 17 | + import { OrbitControls } from './jsm/controls/OrbitControls.js'; |
| 18 | + import { ImprovedNoise } from './jsm/math/ImprovedNoise.js'; |
| 19 | + |
| 20 | + import { GUI } from './jsm/libs/dat.gui.module.js'; |
| 21 | + import { WEBGL } from './jsm/WebGL.js'; |
| 22 | + |
| 23 | + if ( WEBGL.isWebGL2Available() === false ) { |
| 24 | + |
| 25 | + document.body.appendChild( WEBGL.getWebGL2ErrorMessage() ); |
| 26 | + |
| 27 | + } |
| 28 | + |
| 29 | + const INITIAL_CLOUD_SIZE = 128; |
| 30 | + |
| 31 | + let renderer, scene, camera; |
| 32 | + let mesh; |
| 33 | + let prevTime = performance.now(); |
| 34 | + let cloudTexture = null; |
| 35 | + |
| 36 | + init(); |
| 37 | + animate(); |
| 38 | + |
| 39 | + function generateCloudTexture( size, scaleFactor = 1.0 ) { |
| 40 | + |
| 41 | + const data = new Uint8Array( size * size * size ); |
| 42 | + const scale = scaleFactor * 10.0 / size; |
| 43 | + |
| 44 | + let i = 0; |
| 45 | + const perlin = new ImprovedNoise(); |
| 46 | + const vector = new THREE.Vector3(); |
| 47 | + |
| 48 | + for ( let z = 0; z < size; z ++ ) { |
| 49 | + |
| 50 | + for ( let y = 0; y < size; y ++ ) { |
| 51 | + |
| 52 | + for ( let x = 0; x < size; x ++ ) { |
| 53 | + |
| 54 | + const dist = vector.set( x, y, z ).subScalar( size / 2 ).divideScalar( size ).length(); |
| 55 | + const fadingFactor = ( 1.0 - dist ) * ( 1.0 - dist ); |
| 56 | + data[ i ] = ( 128 + 128 * perlin.noise( x * scale / 1.5, y * scale, z * scale / 1.5 ) ) * fadingFactor; |
| 57 | + |
| 58 | + i ++; |
| 59 | + |
| 60 | + } |
| 61 | + |
| 62 | + } |
| 63 | + |
| 64 | + } |
| 65 | + |
| 66 | + return new THREE.DataTexture3D( data, size, size, size ); |
| 67 | + |
| 68 | + } |
| 69 | + |
| 70 | + function init() { |
| 71 | + |
| 72 | + renderer = new THREE.WebGLRenderer(); |
| 73 | + renderer.setPixelRatio( window.devicePixelRatio ); |
| 74 | + renderer.setSize( window.innerWidth, window.innerHeight ); |
| 75 | + document.body.appendChild( renderer.domElement ); |
| 76 | + |
| 77 | + scene = new THREE.Scene(); |
| 78 | + |
| 79 | + camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 100 ); |
| 80 | + camera.position.set( 0, 0, 1.5 ); |
| 81 | + |
| 82 | + new OrbitControls( camera, renderer.domElement ); |
| 83 | + |
| 84 | + // Sky |
| 85 | + |
| 86 | + const canvas = document.createElement( 'canvas' ); |
| 87 | + canvas.width = 1; |
| 88 | + canvas.height = 32; |
| 89 | + |
| 90 | + const context = canvas.getContext( '2d' ); |
| 91 | + const gradient = context.createLinearGradient( 0, 0, 0, 32 ); |
| 92 | + gradient.addColorStop( 0.0, '#014a84' ); |
| 93 | + gradient.addColorStop( 0.5, '#0561a0' ); |
| 94 | + gradient.addColorStop( 1.0, '#437ab6' ); |
| 95 | + context.fillStyle = gradient; |
| 96 | + context.fillRect( 0, 0, 1, 32 ); |
| 97 | + |
| 98 | + const sky = new THREE.Mesh( |
| 99 | + new THREE.SphereGeometry( 10 ), |
| 100 | + new THREE.MeshBasicMaterial( { map: new THREE.CanvasTexture( canvas ), side: THREE.BackSide } ) |
| 101 | + ); |
| 102 | + scene.add( sky ); |
| 103 | + |
| 104 | + // Texture |
| 105 | + |
| 106 | + const texture = new THREE.DataTexture3D( |
| 107 | + new Uint8Array( INITIAL_CLOUD_SIZE * INITIAL_CLOUD_SIZE * INITIAL_CLOUD_SIZE ).fill( 0 ), |
| 108 | + INITIAL_CLOUD_SIZE, |
| 109 | + INITIAL_CLOUD_SIZE, |
| 110 | + INITIAL_CLOUD_SIZE |
| 111 | + ); |
| 112 | + texture.format = THREE.RedFormat; |
| 113 | + texture.minFilter = THREE.LinearFilter; |
| 114 | + texture.magFilter = THREE.LinearFilter; |
| 115 | + texture.unpackAlignment = 1; |
| 116 | + |
| 117 | + cloudTexture = texture; |
| 118 | + |
| 119 | + // Material |
| 120 | + |
| 121 | + const vertexShader = /* glsl */` |
| 122 | + in vec3 position; |
| 123 | +
|
| 124 | + uniform mat4 modelMatrix; |
| 125 | + uniform mat4 modelViewMatrix; |
| 126 | + uniform mat4 projectionMatrix; |
| 127 | + uniform vec3 cameraPos; |
| 128 | +
|
| 129 | + out vec3 vOrigin; |
| 130 | + out vec3 vDirection; |
| 131 | +
|
| 132 | + void main() { |
| 133 | + vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); |
| 134 | +
|
| 135 | + vOrigin = vec3( inverse( modelMatrix ) * vec4( cameraPos, 1.0 ) ).xyz; |
| 136 | + vDirection = position - vOrigin; |
| 137 | +
|
| 138 | + gl_Position = projectionMatrix * mvPosition; |
| 139 | + } |
| 140 | + `; |
| 141 | + |
| 142 | + const fragmentShader = /* glsl */` |
| 143 | + precision highp float; |
| 144 | + precision highp sampler3D; |
| 145 | +
|
| 146 | + uniform mat4 modelViewMatrix; |
| 147 | + uniform mat4 projectionMatrix; |
| 148 | +
|
| 149 | + in vec3 vOrigin; |
| 150 | + in vec3 vDirection; |
| 151 | +
|
| 152 | + out vec4 color; |
| 153 | +
|
| 154 | + uniform vec3 base; |
| 155 | + uniform sampler3D map; |
| 156 | +
|
| 157 | + uniform float threshold; |
| 158 | + uniform float range; |
| 159 | + uniform float opacity; |
| 160 | + uniform float steps; |
| 161 | + uniform float frame; |
| 162 | +
|
| 163 | + uint wang_hash(uint seed) |
| 164 | + { |
| 165 | + seed = (seed ^ 61u) ^ (seed >> 16u); |
| 166 | + seed *= 9u; |
| 167 | + seed = seed ^ (seed >> 4u); |
| 168 | + seed *= 0x27d4eb2du; |
| 169 | + seed = seed ^ (seed >> 15u); |
| 170 | + return seed; |
| 171 | + } |
| 172 | +
|
| 173 | + float randomFloat(inout uint seed) |
| 174 | + { |
| 175 | + return float(wang_hash(seed)) / 4294967296.; |
| 176 | + } |
| 177 | +
|
| 178 | + vec2 hitBox( vec3 orig, vec3 dir ) { |
| 179 | + const vec3 box_min = vec3( - 0.5 ); |
| 180 | + const vec3 box_max = vec3( 0.5 ); |
| 181 | + vec3 inv_dir = 1.0 / dir; |
| 182 | + vec3 tmin_tmp = ( box_min - orig ) * inv_dir; |
| 183 | + vec3 tmax_tmp = ( box_max - orig ) * inv_dir; |
| 184 | + vec3 tmin = min( tmin_tmp, tmax_tmp ); |
| 185 | + vec3 tmax = max( tmin_tmp, tmax_tmp ); |
| 186 | + float t0 = max( tmin.x, max( tmin.y, tmin.z ) ); |
| 187 | + float t1 = min( tmax.x, min( tmax.y, tmax.z ) ); |
| 188 | + return vec2( t0, t1 ); |
| 189 | + } |
| 190 | +
|
| 191 | + float sample1( vec3 p ) { |
| 192 | + return texture( map, p ).r; |
| 193 | + } |
| 194 | +
|
| 195 | + float shading( vec3 coord ) { |
| 196 | + float step = 0.01; |
| 197 | + return sample1( coord + vec3( - step ) ) - sample1( coord + vec3( step ) ); |
| 198 | + } |
| 199 | +
|
| 200 | + void main(){ |
| 201 | + vec3 rayDir = normalize( vDirection ); |
| 202 | + vec2 bounds = hitBox( vOrigin, rayDir ); |
| 203 | +
|
| 204 | + if ( bounds.x > bounds.y ) discard; |
| 205 | +
|
| 206 | + bounds.x = max( bounds.x, 0.0 ); |
| 207 | +
|
| 208 | + vec3 p = vOrigin + bounds.x * rayDir; |
| 209 | + vec3 inc = 1.0 / abs( rayDir ); |
| 210 | + float delta = min( inc.x, min( inc.y, inc.z ) ); |
| 211 | + delta /= steps; |
| 212 | +
|
| 213 | + // Jitter |
| 214 | +
|
| 215 | + // Nice little seed from |
| 216 | + // https://blog.demofox.org/2020/05/25/casual-shadertoy-path-tracing-1-basic-camera-diffuse-emissive/ |
| 217 | + uint seed = uint( gl_FragCoord.x ) * uint( 1973 ) + uint( gl_FragCoord.y ) * uint( 9277 ) + uint( frame ) * uint( 26699 ); |
| 218 | + vec3 size = vec3( textureSize( map, 0 ) ); |
| 219 | + float randNum = randomFloat( seed ) * 2.0 - 1.0; |
| 220 | + p += rayDir * randNum * ( 1.0 / size ); |
| 221 | +
|
| 222 | + // |
| 223 | +
|
| 224 | + vec4 ac = vec4( base, 0.0 ); |
| 225 | +
|
| 226 | + for ( float t = bounds.x; t < bounds.y; t += delta ) { |
| 227 | +
|
| 228 | + float d = sample1( p + 0.5 ); |
| 229 | +
|
| 230 | + d = smoothstep( threshold - range, threshold + range, d ) * opacity; |
| 231 | +
|
| 232 | + float col = shading( p + 0.5 ) * 3.0 + ( ( p.x + p.y ) * 0.25 ) + 0.2; |
| 233 | +
|
| 234 | + ac.rgb += ( 1.0 - ac.a ) * d * col; |
| 235 | +
|
| 236 | + ac.a += ( 1.0 - ac.a ) * d; |
| 237 | +
|
| 238 | + if ( ac.a >= 0.95 ) break; |
| 239 | +
|
| 240 | + p += rayDir * delta; |
| 241 | +
|
| 242 | + } |
| 243 | +
|
| 244 | + color = ac; |
| 245 | +
|
| 246 | + if ( color.a == 0.0 ) discard; |
| 247 | +
|
| 248 | + } |
| 249 | + `; |
| 250 | + |
| 251 | + const geometry = new THREE.BoxGeometry( 1, 1, 1 ); |
| 252 | + const material = new THREE.RawShaderMaterial( { |
| 253 | + glslVersion: THREE.GLSL3, |
| 254 | + uniforms: { |
| 255 | + base: { value: new THREE.Color( 0x798aa0 ) }, |
| 256 | + map: { value: texture }, |
| 257 | + cameraPos: { value: new THREE.Vector3() }, |
| 258 | + threshold: { value: 0.25 }, |
| 259 | + opacity: { value: 0.25 }, |
| 260 | + range: { value: 0.1 }, |
| 261 | + steps: { value: 100 }, |
| 262 | + frame: { value: 0 } |
| 263 | + }, |
| 264 | + vertexShader, |
| 265 | + fragmentShader, |
| 266 | + side: THREE.BackSide, |
| 267 | + transparent: true |
| 268 | + } ); |
| 269 | + |
| 270 | + mesh = new THREE.Mesh( geometry, material ); |
| 271 | + scene.add( mesh ); |
| 272 | + |
| 273 | + // |
| 274 | + |
| 275 | + const parameters = { |
| 276 | + threshold: 0.25, |
| 277 | + opacity: 0.25, |
| 278 | + range: 0.1, |
| 279 | + steps: 100 |
| 280 | + }; |
| 281 | + |
| 282 | + function update() { |
| 283 | + |
| 284 | + material.uniforms.threshold.value = parameters.threshold; |
| 285 | + material.uniforms.opacity.value = parameters.opacity; |
| 286 | + material.uniforms.range.value = parameters.range; |
| 287 | + material.uniforms.steps.value = parameters.steps; |
| 288 | + |
| 289 | + } |
| 290 | + |
| 291 | + const gui = new GUI(); |
| 292 | + gui.add( parameters, 'threshold', 0, 1, 0.01 ).onChange( update ); |
| 293 | + gui.add( parameters, 'opacity', 0, 1, 0.01 ).onChange( update ); |
| 294 | + gui.add( parameters, 'range', 0, 1, 0.01 ).onChange( update ); |
| 295 | + gui.add( parameters, 'steps', 0, 200, 1 ).onChange( update ); |
| 296 | + |
| 297 | + window.addEventListener( 'resize', onWindowResize ); |
| 298 | + |
| 299 | + } |
| 300 | + |
| 301 | + function onWindowResize() { |
| 302 | + |
| 303 | + camera.aspect = window.innerWidth / window.innerHeight; |
| 304 | + camera.updateProjectionMatrix(); |
| 305 | + |
| 306 | + renderer.setSize( window.innerWidth, window.innerHeight ); |
| 307 | + |
| 308 | + } |
| 309 | + |
| 310 | + let curr = 0; |
| 311 | + const countPerRow = 4; |
| 312 | + const countPerSlice = countPerRow * countPerRow; |
| 313 | + const sliceCount = 4; |
| 314 | + const totalCount = sliceCount * countPerSlice; |
| 315 | + const margins = 8; |
| 316 | + |
| 317 | + const perElementPaddedSize = ( INITIAL_CLOUD_SIZE - margins ) / countPerRow; |
| 318 | + const perElementSize = Math.floor( ( INITIAL_CLOUD_SIZE - 1 ) / countPerRow ); |
| 319 | + |
| 320 | + function animate() { |
| 321 | + |
| 322 | + requestAnimationFrame( animate ); |
| 323 | + |
| 324 | + const time = performance.now(); |
| 325 | + if ( time - prevTime > 1500.0 && curr < totalCount ) { |
| 326 | + |
| 327 | + const position = new THREE.Vector3( |
| 328 | + Math.floor( curr % countPerRow ) * perElementSize + margins * 0.5, |
| 329 | + ( Math.floor( ( ( curr % countPerSlice ) / countPerRow ) ) ) * perElementSize + margins * 0.5, |
| 330 | + Math.floor( curr / countPerSlice ) * perElementSize + margins * 0.5 |
| 331 | + ).floor(); |
| 332 | + |
| 333 | + const maxDimension = perElementPaddedSize - 1; |
| 334 | + const box = new THREE.Box3( new THREE.Vector3( 0, 0, 0 ), new THREE.Vector3( maxDimension, maxDimension, maxDimension ) ); |
| 335 | + const scaleFactor = ( Math.random() + 0.5 ) * 0.5; |
| 336 | + const source = generateCloudTexture( perElementPaddedSize, scaleFactor ); |
| 337 | + |
| 338 | + renderer.copyTextureToTexture3D( box, position, source, cloudTexture ); |
| 339 | + |
| 340 | + prevTime = time; |
| 341 | + |
| 342 | + curr ++; |
| 343 | + |
| 344 | + } |
| 345 | + |
| 346 | + mesh.material.uniforms.cameraPos.value.copy( camera.position ); |
| 347 | + // mesh.rotation.y = - performance.now() / 7500; |
| 348 | + |
| 349 | + mesh.material.uniforms.frame.value ++; |
| 350 | + |
| 351 | + renderer.render( scene, camera ); |
| 352 | + |
| 353 | + } |
| 354 | + |
| 355 | + </script> |
| 356 | + |
| 357 | + </body> |
| 358 | +</html> |
0 commit comments