Skip to content

Commit 8d7837f

Browse files
feat(angular-3d): migrate bubble-text and cloud-layer to tsl materials
- Task 2.1: Migrate BubbleText to TSL Material - Replace ShaderMaterial with MeshStandardNodeMaterial - Use tslFresnel and tslIridescence from utilities - Remove 45 lines of GLSL shader code - Configure colorNode, opacityNode for WebGPU native rendering - Task 2.2: Migrate CloudLayer to TSL Material - Replace ShaderMaterial with MeshBasicNodeMaterial - Use applyFog and clampForBloom from utilities - Remove GLSL fog shader code - Works natively on both WebGPU and WebGL backends
1 parent 189bf2d commit 8d7837f

2 files changed

Lines changed: 107 additions & 118 deletions

File tree

libs/angular-3d/src/lib/primitives/cloud-layer.component.ts

Lines changed: 61 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*
44
* Creates realistic cloud layers matching the MrDoob reference.
55
* Uses merged geometries for optimal GPU performance.
6+
* Now uses TSL (Three.js Shading Language) for WebGPU/WebGL native materials.
67
*
78
* @example
89
* ```html
@@ -24,42 +25,17 @@ import {
2425
} from '@angular/core';
2526
import { isPlatformBrowser } from '@angular/common';
2627
import * as THREE from 'three/webgpu';
28+
import { MeshBasicNodeMaterial } from 'three/webgpu';
2729
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
2830
import { NG_3D_PARENT } from '../types/tokens';
2931
import { RenderLoopService } from '../render-loop/render-loop.service';
3032
import { SceneService } from '../canvas/scene.service';
3133
import { injectTextureLoader } from '../loaders/inject-texture-loader';
34+
import { applyFog, clampForBloom } from './shaders/tsl-utilities';
35+
// eslint-disable-next-line @nx/enforce-module-boundaries
36+
import * as TSL from 'three/tsl';
3237

33-
// Cloud shader - exact match to reference
34-
const cloudShader = {
35-
vertexShader: `
36-
varying vec2 vUv;
37-
void main() {
38-
vUv = uv;
39-
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
40-
}
41-
`,
42-
fragmentShader: `
43-
uniform sampler2D map;
44-
uniform vec3 fogColor;
45-
uniform float fogNear;
46-
uniform float fogFar;
47-
varying vec2 vUv;
48-
49-
void main() {
50-
float depth = gl_FragCoord.z / gl_FragCoord.w;
51-
float fogFactor = smoothstep(fogNear, fogFar, depth);
52-
53-
gl_FragColor = texture2D(map, vUv);
54-
gl_FragColor.w *= pow(gl_FragCoord.z, 20.0);
55-
gl_FragColor = mix(gl_FragColor, vec4(fogColor, gl_FragColor.w), fogFactor);
56-
57-
// CRITICAL: Clamp RGB to stay below bloom threshold (0.9)
58-
// This prevents clouds from triggering the bloom effect
59-
gl_FragColor.rgb = min(gl_FragColor.rgb, vec3(0.85));
60-
}
61-
`,
62-
};
38+
const { texture, uv, positionView, float, vec3, pow, color } = TSL;
6339

6440
@Component({
6541
selector: 'a3d-cloud-layer',
@@ -96,7 +72,7 @@ export class CloudLayerComponent {
9672
// Three.js objects
9773
private cloudMesh: THREE.Mesh | null = null;
9874
private cloudMeshBack: THREE.Mesh | null = null;
99-
private material: THREE.ShaderMaterial | null = null;
75+
private material: MeshBasicNodeMaterial | null = null;
10076
private startTime = 0;
10177
private mouseX = 0;
10278
private mouseY = 0;
@@ -118,10 +94,10 @@ export class CloudLayerComponent {
11894

11995
// Create clouds when texture is loaded
12096
effect(() => {
121-
const texture = this.textureResource.data();
122-
if (!texture) return;
97+
const loadedTexture = this.textureResource.data();
98+
if (!loadedTexture) return;
12399

124-
this.createClouds(texture);
100+
this.createClouds(loadedTexture);
125101
});
126102

127103
// Cleanup
@@ -130,7 +106,7 @@ export class CloudLayerComponent {
130106
});
131107
}
132108

133-
private createClouds(texture: THREE.Texture): void {
109+
private createClouds(loadedTexture: THREE.Texture): void {
134110
const parent = this.parentFn?.();
135111
if (!parent) {
136112
console.warn('[CloudLayerComponent] No parent found');
@@ -141,28 +117,12 @@ export class CloudLayerComponent {
141117
this.cleanup();
142118

143119
// Configure texture - exact match to reference
144-
texture.colorSpace = THREE.SRGBColorSpace;
145-
texture.magFilter = THREE.LinearFilter;
146-
texture.minFilter = THREE.LinearMipMapLinearFilter;
147-
148-
// Get fog color
149-
const fogColorValue = this.fogColor();
150-
const fogColorThree = new THREE.Color(fogColorValue);
120+
loadedTexture.colorSpace = THREE.SRGBColorSpace;
121+
loadedTexture.magFilter = THREE.LinearFilter;
122+
loadedTexture.minFilter = THREE.LinearMipMapLinearFilter;
151123

152-
// Create shader material - exact match to reference
153-
this.material = new THREE.ShaderMaterial({
154-
uniforms: {
155-
map: { value: texture },
156-
fogColor: { value: fogColorThree },
157-
fogNear: { value: this.fogNear() },
158-
fogFar: { value: this.fogFar() },
159-
},
160-
vertexShader: cloudShader.vertexShader,
161-
fragmentShader: cloudShader.fragmentShader,
162-
depthWrite: false,
163-
depthTest: false,
164-
transparent: true,
165-
});
124+
// Create TSL-based material
125+
this.material = this.createCloudMaterial(loadedTexture);
166126

167127
// Create merged geometry - EXACT match to reference positioning
168128
const geometry = this.createMergedCloudGeometry();
@@ -189,6 +149,51 @@ export class CloudLayerComponent {
189149
this.startAnimation();
190150
}
191151

152+
/**
153+
* Create TSL-based cloud material
154+
* Replaces GLSL ShaderMaterial with native TSL nodes
155+
*/
156+
private createCloudMaterial(
157+
cloudTexture: THREE.Texture
158+
): MeshBasicNodeMaterial {
159+
const fogColorValue = this.fogColor();
160+
const fogColorThree = new THREE.Color(fogColorValue);
161+
162+
// Create TSL nodes for the material
163+
// Sample the cloud texture
164+
const texColor = texture(cloudTexture, uv());
165+
166+
// Calculate view depth for fog (use view-space z position)
167+
const depth = positionView.z.negate();
168+
169+
// Apply depth-based alpha modification (matches original: pow(gl_FragCoord.z, 20.0))
170+
// Use a power curve to fade clouds based on depth
171+
const depthFade = pow(depth.div(3000).clamp(0, 1), float(0.5));
172+
const alphaWithDepth = texColor.a.mul(depthFade);
173+
174+
// Apply fog to the color
175+
const foggedColor = applyFog(
176+
texColor,
177+
color(fogColorThree),
178+
float(this.fogNear()),
179+
float(this.fogFar()),
180+
depth
181+
);
182+
183+
// Clamp RGB to stay below bloom threshold (0.85)
184+
const clampedColor = clampForBloom(foggedColor, 0.85);
185+
186+
// Create MeshBasicNodeMaterial with TSL nodes
187+
const material = new MeshBasicNodeMaterial();
188+
material.colorNode = clampedColor;
189+
material.opacityNode = alphaWithDepth;
190+
material.transparent = true;
191+
material.depthWrite = false;
192+
material.depthTest = false;
193+
194+
return material;
195+
}
196+
192197
private createMergedCloudGeometry(): THREE.BufferGeometry {
193198
// EXACT match to reference: 64x64 plane
194199
const planeGeo = new THREE.PlaneGeometry(

libs/angular-3d/src/lib/primitives/text/bubble-text.component.ts

Lines changed: 46 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,28 @@ import {
88
signal,
99
} from '@angular/core';
1010
import * as THREE from 'three/webgpu';
11+
import { MeshStandardNodeMaterial } from 'three/webgpu';
1112
import { SceneService } from '../../canvas/scene.service';
1213
import { RenderLoopService } from '../../render-loop/render-loop.service';
1314
import { TextSamplingService } from '../../services/text-sampling.service';
1415
import { OBJECT_ID } from '../../tokens/object-id.token';
1516
import { NG_3D_PARENT } from '../../types/tokens';
17+
import { tslFresnel, tslIridescence } from '../shaders/tsl-utilities';
18+
// eslint-disable-next-line @nx/enforce-module-boundaries
19+
import * as TSL from 'three/tsl';
20+
21+
const { float, vec3, color, mix } = TSL;
1622

1723
/**
1824
* BubbleTextComponent - 3D Bubble Text using Instanced Meshes
1925
*
2026
* Creates transparent bubble spheres that form text shape, inspired by
21-
* Codrops bubble typer effect. Uses IcosahedronGeometry with custom
27+
* Codrops bubble typer effect. Uses IcosahedronGeometry with TSL-based
2228
* rim-lighting shader for realistic soap bubble appearance.
2329
*
2430
* Features:
2531
* - IcosahedronGeometry spheres (not flat planes)
26-
* - Custom bubble shader (transparent center, opaque rim)
32+
* - TSL bubble material (transparent center, opaque rim) - WebGPU native
2733
* - Grow → Burst → Regrow lifecycle animation
2834
* - Optional flying bubbles that float upward
2935
* - Billboard-free (true 3D spheres)
@@ -89,7 +95,7 @@ export class BubbleTextComponent {
8995

9096
// Internal state
9197
private bubbleGeometry?: THREE.IcosahedronGeometry;
92-
private bubbleMaterial?: THREE.ShaderMaterial;
98+
private bubbleMaterial?: MeshStandardNodeMaterial;
9399
private instancedMesh?: THREE.InstancedMesh;
94100
private dummy = new THREE.Object3D();
95101

@@ -132,7 +138,7 @@ export class BubbleTextComponent {
132138
if (this.instancedMesh && parent) {
133139
parent.remove(this.instancedMesh);
134140
this.instancedMesh.geometry.dispose();
135-
(this.instancedMesh.material as THREE.ShaderMaterial).dispose();
141+
(this.instancedMesh.material as MeshStandardNodeMaterial).dispose();
136142
}
137143
if (this.bubbleGeometry) {
138144
this.bubbleGeometry.dispose();
@@ -152,7 +158,7 @@ export class BubbleTextComponent {
152158
if (this.instancedMesh && parent) {
153159
parent.remove(this.instancedMesh);
154160
this.instancedMesh.geometry.dispose();
155-
(this.instancedMesh.material as THREE.ShaderMaterial).dispose();
161+
(this.instancedMesh.material as MeshStandardNodeMaterial).dispose();
156162
this.instancedMesh = undefined;
157163
}
158164
this.bubbles = [];
@@ -243,18 +249,16 @@ export class BubbleTextComponent {
243249
if (this.instancedMesh && parent) {
244250
parent.remove(this.instancedMesh);
245251
this.instancedMesh.geometry.dispose();
246-
(this.instancedMesh.material as THREE.ShaderMaterial).dispose();
252+
(this.instancedMesh.material as MeshStandardNodeMaterial).dispose();
247253
}
248254

249255
// Create geometry (Icosahedron for spherical bubbles)
250256
if (!this.bubbleGeometry) {
251257
this.bubbleGeometry = new THREE.IcosahedronGeometry(1, 2); // Detail level 2
252258
}
253259

254-
// Create bubble shader material
255-
if (!this.bubbleMaterial) {
256-
this.bubbleMaterial = this.createBubbleShaderMaterial();
257-
}
260+
// Create TSL bubble material
261+
this.bubbleMaterial = this.createBubbleMaterial();
258262

259263
// Create instanced mesh
260264
this.instancedMesh = new THREE.InstancedMesh(
@@ -273,59 +277,39 @@ export class BubbleTextComponent {
273277
}
274278

275279
/**
276-
* Create bubble shader material with rim transparency effect
280+
* Create TSL-based bubble material with rim transparency effect
281+
* Uses native TSL nodes for WebGPU/WebGL compatibility
277282
*/
278-
private createBubbleShaderMaterial(): THREE.ShaderMaterial {
279-
const bubbleColor = new THREE.Color(this.bubbleColor());
280-
281-
return new THREE.ShaderMaterial({
282-
uniforms: {
283-
uColor: { value: bubbleColor },
284-
uOpacity: { value: this.opacity() },
285-
},
286-
vertexShader: `
287-
varying vec3 vNormal;
288-
varying vec3 vViewPosition;
289-
290-
void main() {
291-
vNormal = normalize(normalMatrix * normal);
292-
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
293-
vViewPosition = -mvPosition.xyz;
294-
gl_Position = projectionMatrix * mvPosition;
295-
}
296-
`,
297-
fragmentShader: `
298-
uniform vec3 uColor;
299-
uniform float uOpacity;
300-
301-
varying vec3 vNormal;
302-
varying vec3 vViewPosition;
303-
304-
void main() {
305-
// Fresnel-like rim effect
306-
vec3 viewDir = normalize(vViewPosition);
307-
float rim = 1.0 - abs(dot(vNormal, viewDir));
308-
rim = pow(rim, 2.0);
309-
310-
// Mix white center with colored rim (bubble refraction look)
311-
vec3 centerColor = vec3(1.0, 1.0, 1.0);
312-
vec3 rimColor = uColor;
313-
vec3 finalColor = mix(centerColor, rimColor, rim * 0.7);
314-
315-
// Add rainbow iridescence
316-
float rainbow = sin(rim * 6.28) * 0.5 + 0.5;
317-
finalColor += vec3(rainbow * 0.1, rainbow * 0.05, rainbow * 0.15);
318-
319-
// Alpha: more transparent at center, more opaque at rim
320-
float alpha = (0.2 + rim * 0.6) * uOpacity;
321-
322-
gl_FragColor = vec4(finalColor, alpha);
323-
}
324-
`,
325-
transparent: true,
326-
side: THREE.DoubleSide,
327-
depthWrite: false,
328-
});
283+
private createBubbleMaterial(): MeshStandardNodeMaterial {
284+
const bubbleColorValue = new THREE.Color(this.bubbleColor());
285+
const opacityValue = this.opacity();
286+
287+
// Create TSL color node
288+
const baseColor = color(bubbleColorValue);
289+
290+
// Calculate fresnel rim effect using TSL utilities
291+
const fresnelValue = tslFresnel(float(2.0), float(0.6), float(0.2));
292+
293+
// Mix white center with colored rim (bubble refraction look)
294+
const centerColor = vec3(1, 1, 1);
295+
const finalColor = mix(centerColor, baseColor, fresnelValue.mul(0.7));
296+
297+
// Add rainbow iridescence using TSL utilities
298+
const iridescent = tslIridescence(fresnelValue, float(0.1));
299+
const colorWithIridescence = finalColor.add(iridescent);
300+
301+
// Alpha: more transparent at center, more opaque at rim
302+
const alpha = float(0.2).add(fresnelValue.mul(0.6)).mul(opacityValue);
303+
304+
// Create MeshStandardNodeMaterial with TSL nodes
305+
const material = new MeshStandardNodeMaterial();
306+
material.colorNode = colorWithIridescence;
307+
material.opacityNode = alpha;
308+
material.transparent = true;
309+
material.side = THREE.DoubleSide;
310+
material.depthWrite = false;
311+
312+
return material;
329313
}
330314

331315
/**

0 commit comments

Comments
 (0)