Skip to content

Commit 55ce4e7

Browse files
committed
docs: port tsl example from threlte
1 parent a107bc4 commit 55ce4e7

File tree

7 files changed

+295
-0
lines changed

7 files changed

+295
-0
lines changed

apps/examples/src/app/misc/misc.routes.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ const routes: Routes = [
3737
},
3838
},
3939
},
40+
{
41+
path: 'webgpu-tsl',
42+
loadComponent: () => import('./webgpu-tsl/webgpu-tsl'),
43+
data: {
44+
credits: {
45+
title: "Threlte's WebGPU TSL example",
46+
link: 'https://threlte.xyz/docs/learn/advanced/webgpu#tsl',
47+
},
48+
},
49+
},
4050
{
4151
path: 'particle-maxime',
4252
loadComponent: () => import('./particle-maxime/particle-maxime'),
76.5 KB
Binary file not shown.
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
computed,
5+
CUSTOM_ELEMENTS_SCHEMA,
6+
effect,
7+
ElementRef,
8+
signal,
9+
viewChild,
10+
} from '@angular/core';
11+
import { extend, injectBeforeRender, injectStore } from 'angular-three';
12+
import { NgtsPerspectiveCamera } from 'angular-three-soba/cameras';
13+
import { NgtsOrbitControls } from 'angular-three-soba/controls';
14+
import { injectGLTF } from 'angular-three-soba/loaders';
15+
import * as THREE from 'three/webgpu';
16+
17+
import { NgTemplateOutlet } from '@angular/common';
18+
import { NgtsEnvironment } from 'angular-three-soba/staging';
19+
import { NgtTweakCheckbox, NgtTweakColor, NgtTweakNumber, NgtTweakPane } from 'angular-three-tweakpane';
20+
import { GLTF } from 'three-stdlib';
21+
import gearsGLB from './gears.glb' with { loader: 'file' };
22+
import { SliceMaterial } from './slice-material';
23+
24+
injectGLTF.preload(() => gearsGLB);
25+
26+
interface GearsGLB extends GLTF {
27+
nodes: {
28+
axle: THREE.Mesh;
29+
gears: THREE.Mesh;
30+
outerHull: THREE.Mesh;
31+
};
32+
}
33+
34+
@Component({
35+
selector: 'app-scene-graph',
36+
template: `
37+
<ngts-perspective-camera [options]="{ makeDefault: true, position: [-5, 5, 12] }" />
38+
39+
<ngt-directional-light
40+
castShadow
41+
[intensity]="4"
42+
[position]="[6.25, 3, 4]"
43+
[shadow.camera.near]="0.1"
44+
[shadow.camera.far]="30"
45+
[shadow.camera.left]="-8"
46+
[shadow.camera.right]="8"
47+
[shadow.camera.top]="8"
48+
[shadow.camera.bottom]="-8"
49+
[shadow.camera.normalBias]="0.05"
50+
[shadow.mapSize.x]="2048"
51+
[shadow.mapSize.y]="2048"
52+
/>
53+
54+
<ng-template #mesh let-mesh>
55+
<ngt-mesh
56+
[geometry]="mesh.geometry"
57+
[scale]="mesh.scale"
58+
[position]="mesh.position"
59+
[rotation]="mesh.rotation"
60+
castShadow
61+
receiveShadow
62+
>
63+
<ngt-mesh-physical-material [parameters]="{ metalness, roughness, envMapIntensity, color }" />
64+
</ngt-mesh>
65+
</ng-template>
66+
67+
<ngt-group #gears [position]="gearsPosition">
68+
@if (gltf(); as gltf) {
69+
@let gears = gltf.nodes.gears;
70+
@let axle = gltf.nodes.axle;
71+
@let outerHull = gltf.nodes.outerHull;
72+
73+
<ng-container [ngTemplateOutlet]="mesh" [ngTemplateOutletContext]="{ $implicit: gears }" />
74+
<ng-container [ngTemplateOutlet]="mesh" [ngTemplateOutletContext]="{ $implicit: axle }" />
75+
76+
<ngt-mesh
77+
[geometry]="outerHull.geometry"
78+
[scale]="outerHull.scale"
79+
[position]="outerHull.position"
80+
[rotation]="outerHull.rotation"
81+
castShadow
82+
receiveShadow
83+
>
84+
<ngt-mesh-physical-node-material
85+
slice
86+
[arcAngle]="arcAngle()"
87+
[startAngle]="startAngle()"
88+
[sliceColor]="sliceColor()"
89+
[parameters]="{ metalness, roughness, envMapIntensity, color, side: DoubleSide }"
90+
/>
91+
</ngt-mesh>
92+
}
93+
</ngt-group>
94+
95+
<ngt-mesh #plane [position]="[-4, -3, -4]" [scale]="10" receiveShadow>
96+
<ngt-plane-geometry />
97+
<ngt-mesh-standard-material color="#aaaaaa" />
98+
</ngt-mesh>
99+
100+
<ngts-environment [options]="{ preset: 'warehouse', background: true, blur: 0.5 }" />
101+
<ngts-orbit-controls />
102+
103+
<ngt-tweak-pane title="Slice Material" left="8px">
104+
<ngt-tweak-checkbox [(value)]="rotate" label="rotate" />
105+
<ngt-tweak-color [(value)]="sliceColor" label="slice color" />
106+
<ngt-tweak-number
107+
[(value)]="startAngleDegrees"
108+
label="start angle degrees"
109+
debounce="0"
110+
[params]="{ min: 0, max: 360, step: 1 }"
111+
/>
112+
<ngt-tweak-number
113+
[(value)]="arcAngleDegrees"
114+
label="arc angle degrees"
115+
debounce="0"
116+
[params]="{ min: 0, max: 360, step: 1 }"
117+
/>
118+
</ngt-tweak-pane>
119+
`,
120+
imports: [
121+
NgtsPerspectiveCamera,
122+
NgtsOrbitControls,
123+
NgtsEnvironment,
124+
NgTemplateOutlet,
125+
SliceMaterial,
126+
NgtTweakPane,
127+
NgtTweakColor,
128+
NgtTweakCheckbox,
129+
NgtTweakNumber,
130+
],
131+
changeDetection: ChangeDetectionStrategy.OnPush,
132+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
133+
})
134+
export class SceneGraph {
135+
protected readonly DoubleSide = THREE.DoubleSide;
136+
137+
private gearsRef = viewChild.required<ElementRef<THREE.Group>>('gears');
138+
private planeRef = viewChild.required<ElementRef<THREE.Mesh>>('plane');
139+
140+
protected gltf = injectGLTF<GearsGLB>(() => gearsGLB);
141+
private store = injectStore();
142+
143+
protected metalness = 0.5;
144+
protected roughness = 0.25;
145+
protected envMapIntensity = 0.5;
146+
protected color = '#858080';
147+
148+
protected gearsPosition = new THREE.Vector3();
149+
150+
protected rotate = signal(true);
151+
protected sliceColor = signal('#9370DB');
152+
protected startAngleDegrees = signal(60);
153+
protected arcAngleDegrees = signal(90);
154+
155+
protected arcAngle = computed(() => THREE.MathUtils.DEG2RAD * this.arcAngleDegrees());
156+
protected startAngle = computed(() => THREE.MathUtils.DEG2RAD * this.startAngleDegrees());
157+
158+
constructor() {
159+
extend(THREE);
160+
161+
injectBeforeRender(({ delta }) => {
162+
const [gears, plane] = [this.gearsRef().nativeElement, this.planeRef().nativeElement];
163+
plane.lookAt(gears.position);
164+
165+
if (!this.rotate()) return;
166+
167+
gears.rotation.y += 0.1 * delta;
168+
});
169+
170+
effect((onCleanup) => {
171+
const [scene, gl] = [this.store.scene(), this.store.gl()];
172+
173+
const blurriness = scene.backgroundBlurriness;
174+
const lastToneMapping = gl.toneMapping;
175+
176+
scene.backgroundBlurriness = 0.5;
177+
gl.toneMapping = THREE.ACESFilmicToneMapping;
178+
179+
onCleanup(() => {
180+
scene.backgroundBlurriness = blurriness;
181+
gl.toneMapping = lastToneMapping;
182+
});
183+
});
184+
}
185+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Directive, effect, ElementRef, inject, input } from '@angular/core';
2+
import { color, uniform } from 'three/tsl';
3+
import * as THREE from 'three/webgpu';
4+
import { outputNodeFn, shadowNodeFn } from './tsl';
5+
6+
@Directive({ selector: 'ngt-mesh-physical-node-material[slice]' })
7+
export class SliceMaterial {
8+
arcAngle = input(0.5 * Math.PI);
9+
startAngle = input(0);
10+
sliceColor = input('black');
11+
12+
private material = inject<ElementRef<THREE.MeshPhysicalNodeMaterial>>(ElementRef);
13+
14+
private uArcAngle = uniform(0.5 * Math.PI);
15+
private uStartAngle = uniform(0);
16+
private uColor = uniform(color('black'));
17+
18+
constructor() {
19+
this.material.nativeElement.outputNode = outputNodeFn({
20+
startAngle: this.uStartAngle,
21+
arcAngle: this.uArcAngle,
22+
color: this.uColor,
23+
});
24+
this.material.nativeElement.castShadowNode = shadowNodeFn({
25+
startAngle: this.uStartAngle,
26+
arcAngle: this.uArcAngle,
27+
});
28+
29+
effect(() => {
30+
const [arcAngle, startAngle, sliceColor] = [this.arcAngle(), this.startAngle(), this.sliceColor()];
31+
this.uStartAngle.value = arcAngle;
32+
this.uArcAngle.value = startAngle;
33+
this.uColor.value.set(sliceColor);
34+
});
35+
}
36+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { NodeRepresentation, ShaderNodeObject } from 'three/tsl';
2+
import { Fn, If, PI2, atan, frontFacing, output, positionLocal, vec4 } from 'three/tsl';
3+
import type { Node } from 'three/webgpu';
4+
5+
type AngleInputs = { startAngle: NodeRepresentation; arcAngle: NodeRepresentation };
6+
7+
const inAngle = Fn(
8+
([position, startAngle, endAngle]: [ShaderNodeObject<Node>, NodeRepresentation, NodeRepresentation]) => {
9+
const angle = atan(position.y, position.x).sub(startAngle).mod(PI2).toVar();
10+
return angle.greaterThan(0).and(angle.lessThan(endAngle));
11+
},
12+
);
13+
14+
export const outputNodeFn = Fn(({ startAngle, arcAngle, color }: AngleInputs & { color: NodeRepresentation }) => {
15+
inAngle(positionLocal.xy, startAngle, arcAngle).discard();
16+
const finalOutput = output;
17+
If(frontFacing.not(), () => {
18+
finalOutput.assign(vec4(color, 1));
19+
});
20+
return finalOutput;
21+
});
22+
23+
export const shadowNodeFn = Fn(({ startAngle, arcAngle }: AngleInputs) => {
24+
inAngle(positionLocal.xy, startAngle, arcAngle).discard();
25+
return vec4(0, 0, 0, 1);
26+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
2+
import { NgtFrameloop, NgtGLOptions } from 'angular-three';
3+
import { NgtCanvas } from 'angular-three/dom';
4+
import * as THREE from 'three/webgpu';
5+
import { SceneGraph } from './scene';
6+
7+
@Component({
8+
template: `
9+
<ngt-canvas shadows [gl]="glFactory" [frameloop]="frameloop()">
10+
<app-scene-graph *canvasContent />
11+
</ngt-canvas>
12+
`,
13+
changeDetection: ChangeDetectionStrategy.OnPush,
14+
imports: [NgtCanvas, SceneGraph],
15+
host: { class: 'webgpu-tsl' },
16+
})
17+
export default class WebGPUTSL {
18+
protected frameloop = signal<NgtFrameloop>('never');
19+
protected glFactory: NgtGLOptions = (defaultOptions) => {
20+
const renderer = new THREE.WebGPURenderer({
21+
canvas: defaultOptions.canvas as HTMLCanvasElement,
22+
powerPreference: 'high-performance',
23+
antialias: true,
24+
forceWebGL: false,
25+
});
26+
27+
renderer.init().then(() => {
28+
this.frameloop.set('always');
29+
});
30+
31+
return renderer;
32+
};
33+
}

apps/examples/src/types.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,8 @@ declare module '*.gltf' {
1717
const url: string;
1818
export default url;
1919
}
20+
21+
declare module '*.hdr' {
22+
const data: string;
23+
export default data;
24+
}

0 commit comments

Comments
 (0)