Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions examples/jsm/tsl/utils/Raymarching.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { varying, vec4, modelWorldMatrixInverse, cameraPosition, positionGeometry, float, Fn, Loop, max, min, vec2, vec3, smoothstep, If, Break } from 'three/tsl';

const hitBox = /*@__PURE__*/ Fn( ( { orig, dir } ) => {

const box_min = vec3( - 0.5 );
const box_max = vec3( 0.5 );

const inv_dir = dir.reciprocal();

const tmin_tmp = box_min.sub( orig ).mul( inv_dir );
const tmax_tmp = box_max.sub( orig ).mul( inv_dir );

const tmin = min( tmin_tmp, tmax_tmp );
const tmax = max( tmin_tmp, tmax_tmp );

const t0 = max( tmin.x, max( tmin.y, tmin.z ) );
const t1 = min( tmax.x, min( tmax.y, tmax.z ) );

return vec2( t0, t1 );

} );

/**
* Performs raymarching box-area using the specified number of steps and a callback function.
*
* ```js
* RaymarchingBox( count, ( { positionRay } ) => {
*
* } );
* ```
*
* @tsl
* @function
* @param {Number|Node} steps - The number of steps for raymarching.
* @param {Function|FunctionNode} callback - The callback function to execute at each step.
* @returns {void}
*/
export const RaymarchingBox = ( steps, callback ) => {

const vOrigin = varying( vec3( modelWorldMatrixInverse.mul( vec4( cameraPosition, 1.0 ) ) ) );
const vDirection = varying( positionGeometry.sub( vOrigin ) );

const rayDir = vDirection.normalize();
const bounds = vec2( hitBox( { orig: vOrigin, dir: rayDir } ) ).toVar();

bounds.x.greaterThan( bounds.y ).discard();

bounds.assign( vec2( max( bounds.x, 0.0 ), bounds.y ) );

const inc = vec3( rayDir.abs().reciprocal() ).toVar();
const delta = float( min( inc.x, min( inc.y, inc.z ) ) ).toVar( 'rayDelta' ); // used 'rayDelta' name in loop

delta.divAssign( float( steps ) );

const positionRay = vec3( vOrigin.add( bounds.x.mul( rayDir ) ) ).toVar();

Loop( { type: 'float', start: bounds.x, end: bounds.y, update: '+= rayDelta' }, () => {

callback( { positionRay } );

positionRay.addAssign( rayDir.mul( delta ) );

} );

};

/**
* Performs raymarching on the specified 3D texture.
*
* @tsl
* @function
* @param {Object} params - The parameters for the function.
* @param {Texture|Node} params.texture - The 3D texture to sample.
* @param {Number|Node} [params.range=0.1] - The range for the smoothstep function.
* @param {Number|Node} [params.threshold=0.25] - The threshold for the smoothstep function.
* @param {Number|Node} [params.opacity=0.25] - The opacity value for the final color.
* @param {Number|Node} [params.steps=100] - The number of steps for raymarching.
* @returns {Function} The generated function that performs raymarching on the 3D texture.
*/
export const raymarchingTexture3D = Fn( ( {
texture,
range = float( 0.1 ),
threshold = float( 0.25 ),
opacity = float( 0.25 ),
steps = float( 100 )
} ) => {

const finalColor = vec4( 0 ).toVar();

RaymarchingBox( steps, ( { positionRay } ) => {

const mapValue = float( texture.sample( positionRay.add( 0.5 ) ).r ).toVar();

mapValue.assign( smoothstep( threshold.sub( range ), threshold.add( range ), mapValue ).mul( opacity ) );

const shading = texture.sample( positionRay.add( vec3( - 0.01 ) ) ).r.sub( texture.sample( positionRay.add( vec3( 0.01 ) ) ).r );

const col = shading.mul( 3.0 ).add( positionRay.x.add( positionRay.y ).mul( 0.25 ) ).add( 0.2 );

finalColor.rgb.addAssign( finalColor.a.oneMinus().mul( mapValue ).mul( col ) );

finalColor.a.addAssign( finalColor.a.oneMinus().mul( mapValue ) );

If( finalColor.a.greaterThanEqual( 0.95 ), () => {

Break();

} );

} );

return finalColor;

} );
79 changes: 26 additions & 53 deletions examples/webgpu_volume_cloud.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
<script type="module">

import * as THREE from 'three';
import { vec3, materialReference, smoothstep, If, Break, Fn } from 'three/tsl';
import { texture3D, uniform } from 'three/tsl';

import { raymarchingTexture3D } from 'three/addons/tsl/utils/Raymarching.js';

import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { ImprovedNoise } from 'three/addons/math/ImprovedNoise.js';
Expand Down Expand Up @@ -109,72 +111,43 @@
texture.unpackAlignment = 1;
texture.needsUpdate = true;

// Cloud Shader

const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const baseColor = uniform( new THREE.Color( 0x798aa0 ) );
const range = uniform( 0.1 );
const threshold = uniform( 0.25 );
const opacity = uniform( 0.25 );
const steps = uniform( 100 );

const material = new THREE.VolumeNodeMaterial( {
side: THREE.BackSide,
transparent: true
const cloud3d = raymarchingTexture3D( {
texture: texture3D( texture, null, 0 ),
range: range,
threshold: threshold,
opacity: opacity,
steps: steps
} );

material.map = texture;
material.base = new THREE.Color( 0x798aa0 );
material.steps = 100;
material.range = 0.1;
material.threshold = 0.25;
material.opacity = 0.25;

const range = materialReference( 'range', 'float' );
const threshold = materialReference( 'threshold', 'float' );
const opacity = materialReference( 'opacity', 'float' );

material.testNode = Fn( ( { map, mapValue, probe, finalColor } ) => {

mapValue.assign( smoothstep( threshold.sub( range ), threshold.add( range ), mapValue ).mul( opacity ) );

const shading = map.sample( probe.add( vec3( - 0.01 ) ) ).r.sub( map.sample( probe.add( vec3( 0.01 ) ) ).r );

const col = shading.mul( 3.0 ).add( probe.x.add( probe.y ).mul( 0.25 ) ).add( 0.2 );

finalColor.rgb.addAssign( finalColor.a.oneMinus().mul( mapValue ).mul( col ) );

finalColor.a.addAssign( finalColor.a.oneMinus().mul( mapValue ) );

If( finalColor.a.greaterThanEqual( 0.95 ), () => {
const finalCloud = cloud3d.setRGB( cloud3d.rgb.add( baseColor ) );

Break();
//

} );
const geometry = new THREE.BoxGeometry( 1, 1, 1 );

} );
const material = new THREE.NodeMaterial();
material.colorNode = finalCloud;
material.side = THREE.BackSide;
material.transparent = true;

mesh = new THREE.Mesh( geometry, material );

scene.add( mesh );

//

const parameters = {
threshold: 0.25,
opacity: 0.25,
range: 0.1,
steps: 100
};

function update() {

material.threshold = parameters.threshold;
material.opacity = parameters.opacity;
material.range = parameters.range;
material.steps = parameters.steps;

}

const gui = new GUI();
gui.add( parameters, 'threshold', 0, 1, 0.01 ).onChange( update );
gui.add( parameters, 'opacity', 0, 1, 0.01 ).onChange( update );
gui.add( parameters, 'range', 0, 1, 0.01 ).onChange( update );
gui.add( parameters, 'steps', 0, 200, 1 ).onChange( update );
gui.add( threshold, 'value', 0, 1, 0.01 ).name( 'threshold' );
gui.add( opacity, 'value', 0, 1, 0.01 ).name( 'opacity' );
gui.add( range, 'value', 0, 1, 0.01 ).name( 'range' );
gui.add( steps, 'value', 0, 200, 1 ).name( 'steps' );

window.addEventListener( 'resize', onWindowResize );

Expand Down