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
119 changes: 109 additions & 10 deletions src/rendering/effects/AdvancedParticleSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/

import * as THREE from 'three';
import { MeshBasicNodeMaterial } from 'three/webgpu';
import {
Fn,
vec2,
Expand Down Expand Up @@ -40,6 +41,10 @@ import {
positionLocal,
modelWorldMatrix,
cameraPosition,
modelViewProjection,
cameraNear,
cameraFar,
viewportCoordinate,
} from 'three/tsl';

// ============================================
Expand Down Expand Up @@ -285,16 +290,20 @@ export class AdvancedParticleSystem {

// Geometry and materials
private geometry: THREE.PlaneGeometry;
private material: THREE.MeshBasicMaterial; // Will be enhanced with custom shader
private material: MeshBasicNodeMaterial;

// Textures
private fireTexture: THREE.Texture;
private smokeTexture: THREE.Texture;
private glowTexture: THREE.Texture;

// Uniforms
// Uniforms for TSL soft particles
private timeUniform: { value: number };
private lightDirUniform: { value: THREE.Vector3 };
private depthTextureUniform: ReturnType<typeof uniform>;
private softnessUniform: ReturnType<typeof uniform>;
private cameraNearUniform: ReturnType<typeof uniform>;
private cameraFarUniform: ReturnType<typeof uniform>;

// Terrain height function
private getTerrainHeight: ((x: number, z: number) => number) | null = null;
Expand Down Expand Up @@ -352,14 +361,24 @@ export class AdvancedParticleSystem {
// Create billboard geometry
this.geometry = new THREE.PlaneGeometry(1, 1);

// Create material with additive blending for glowing particles
this.material = new THREE.MeshBasicMaterial({
map: this.glowTexture,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide,
});
// Initialize TSL uniforms for soft particles
// Create a placeholder 1x1 depth texture (will be set properly via setDepthTexture)
const placeholderDepth = new THREE.DataTexture(
new Float32Array([1.0]),
1,
1,
THREE.RedFormat,
THREE.FloatType
);
placeholderDepth.needsUpdate = true;

this.depthTextureUniform = uniform(placeholderDepth);
this.softnessUniform = uniform(0.5); // Default softness
this.cameraNearUniform = uniform(0.1);
this.cameraFarUniform = uniform(1000.0);

// Create TSL soft particle material with depth-aware alpha fade
this.material = this.createSoftParticleMaterial();

// Create instanced mesh
this.instancedMesh = new THREE.InstancedMesh(
Expand Down Expand Up @@ -505,6 +524,86 @@ export class AdvancedParticleSystem {
return texture;
}

/**
* Create TSL soft particle material with depth-aware alpha fade
* Particles fade when approaching scene geometry to avoid hard clipping
*/
private createSoftParticleMaterial(): MeshBasicNodeMaterial {
const glowTex = this.glowTexture;
const depthTex = this.depthTextureUniform;
const softness = this.softnessUniform;
const near = this.cameraNearUniform;
const far = this.cameraFarUniform;

// TSL fragment shader for soft particles
const softParticleFragment = Fn(() => {
// Sample the glow texture
const texCoord = uv();
const texColor = texture(glowTex, texCoord);

// Get screen-space coordinates for depth sampling
const screenUV = viewportCoordinate.xy;

// Sample scene depth and convert to linear
const sceneDepthSample = texture(depthTex, screenUV).r;
Comment on lines +544 to +548

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Sample depth with normalized viewport UVs

viewportCoordinate is defined in TSL as pixel-space coordinates (see node_modules/three/src/nodes/display/ScreenNode.js around the viewportCoordinate/viewportUV definitions). Passing pixel units directly into texture() means the depth lookup will be out of [0,1] for most fragments and clamp to edges, so the soft-fade will read a near-constant depth and won’t correctly fade near geometry. Use viewportUV (or divide by viewportSize) for normalized sampling.

Useful? React with 👍 / 👎.

const sceneNDC = sceneDepthSample.mul(2.0).sub(1.0);
const sceneLinearDepth = near.mul(far).div(
far.sub(sceneNDC.mul(far.sub(near)))
);

// Get particle's linear depth from gl_FragCoord.z
const particleNDC = viewportCoordinate.z.mul(2.0).sub(1.0);
const particleLinearDepth = near.mul(far).div(
Comment on lines +554 to +556

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid swizzling .z from viewportCoordinate

viewportCoordinate is a vec2 in TSL (pixel-space x/y), so viewportCoordinate.z is an invalid swizzle. This will emit an invalid shader expression or undefined value for the particle depth, breaking the linear-depth calculation and potentially failing material compilation. Use a depth-specific node like viewportDepth/viewportLinearDepth or another supported fragment-depth source instead.

Useful? React with 👍 / 👎.

far.sub(particleNDC.mul(far.sub(near)))
);

// Soft particle fade: alpha decreases as particle approaches scene geometry
// depthDiff is positive when particle is in front of scene geometry
const depthDiff = sceneLinearDepth.sub(particleLinearDepth);
const softFade = clamp(depthDiff.div(softness), 0.0, 1.0);

// Apply soft fade to texture alpha
const finalAlpha = texColor.a.mul(softFade);

return vec4(texColor.rgb, finalAlpha);
});

// Create MeshBasicNodeMaterial with custom fragment
const material = new MeshBasicNodeMaterial();
material.transparent = true;
material.depthWrite = false;
material.blending = THREE.AdditiveBlending;
material.side = THREE.DoubleSide;

// Set the color/alpha output using the soft particle node
material.colorNode = softParticleFragment();

return material;
}

/**
* Set the depth texture for soft particle rendering
* Should be called with the scene's depth buffer texture
*/
public setDepthTexture(depthTexture: THREE.Texture): void {
this.depthTextureUniform.value = depthTexture;
}

/**
* Update camera parameters for soft particle depth calculations
*/
public updateCamera(camera: THREE.PerspectiveCamera): void {
this.cameraNearUniform.value = camera.near;
this.cameraFarUniform.value = camera.far;
}

/**
* Set the softness value for depth fade (higher = softer transition)
*/
public setSoftness(softness: number): void {
this.softnessUniform.value = softness;
}

/**
* Emit particles at a position
*/
Expand Down
9 changes: 8 additions & 1 deletion src/rendering/tsl/VolumetricFog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
abs,
Loop,
If,
Break,
} from 'three/tsl';

// Quality presets - number of raymarch steps
Expand Down Expand Up @@ -135,8 +136,14 @@ export function createVolumetricFogNode(
const inScattering = vec3(0.0).toVar();

// Raymarch loop with manual counter
// Use max quality (128) with early exit for dynamic quality control
const loopIndex = int(0).toVar();
Loop(32, () => {
Loop(128, () => {
// Early exit when we've done enough steps for current quality
If(loopIndex.greaterThanEqual(uSteps), () => {
Break();
});

const t = float(loopIndex).mul(stepSize);
const samplePos = uCameraPos.add(rayDir.mul(t));

Expand Down