Skip to content

Commit 33f97e2

Browse files
committed
wireframe
1 parent ad67270 commit 33f97e2

File tree

7 files changed

+791
-0
lines changed

7 files changed

+791
-0
lines changed

libs/soba/shaders/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ export * from './soft-shadow-material/soft-shadow-material';
1414
export * from './sparkles-material/sparkles-material';
1515
export * from './spot-light-material/spot-light-material';
1616
export * from './star-field-material/star-field-material';
17+
export * from './wireframe-material/wireframe-material';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
import { computed, effect, runInInjectionContext, type Injector } from '@angular/core';
2+
import { assertInjectionContext, type NgtShaderMaterial } from 'angular-three';
3+
import * as THREE from 'three';
4+
import { shaderMaterial } from '../shader-material/shader-material';
5+
6+
export type WireframeMaterialState = {
7+
fillOpacity?: number;
8+
fillMix?: number;
9+
strokeOpacity?: number;
10+
thickness?: number;
11+
colorBackfaces?: boolean;
12+
dashInvert?: boolean;
13+
dash?: boolean;
14+
dashRepeats?: number;
15+
dashLength?: number;
16+
squeeze?: boolean;
17+
squeezeMin?: number;
18+
squeezeMax?: number;
19+
stroke?: THREE.ColorRepresentation;
20+
backfaceStroke?: THREE.ColorRepresentation;
21+
fill?: THREE.ColorRepresentation;
22+
};
23+
24+
export const WireframeMaterialShaders = {
25+
uniforms: {
26+
strokeOpacity: 1,
27+
fillOpacity: 0.25,
28+
fillMix: 0,
29+
thickness: 0.05,
30+
colorBackfaces: false,
31+
dashInvert: true,
32+
dash: false,
33+
dashRepeats: 4,
34+
dashLength: 0.5,
35+
squeeze: false,
36+
squeezeMin: 0.2,
37+
squeezeMax: 1,
38+
stroke: new THREE.Color('#ff0000'),
39+
backfaceStroke: new THREE.Color('#0000ff'),
40+
fill: new THREE.Color('#00ff00'),
41+
},
42+
vertex: /* glsl */ `
43+
attribute vec3 barycentric;
44+
45+
varying vec3 v_edges_Barycentric;
46+
varying vec3 v_edges_Position;
47+
48+
void initWireframe() {
49+
v_edges_Barycentric = barycentric;
50+
v_edges_Position = position.xyz;
51+
}
52+
`,
53+
fragment: /* glsl */ `
54+
#ifndef PI
55+
#define PI 3.1415926535897932384626433832795
56+
#endif
57+
58+
varying vec3 v_edges_Barycentric;
59+
varying vec3 v_edges_Position;
60+
61+
uniform float strokeOpacity;
62+
uniform float fillOpacity;
63+
uniform float fillMix;
64+
uniform float thickness;
65+
uniform bool colorBackfaces;
66+
67+
// Dash
68+
uniform bool dashInvert;
69+
uniform bool dash;
70+
uniform bool dashOnly;
71+
uniform float dashRepeats;
72+
uniform float dashLength;
73+
74+
// Squeeze
75+
uniform bool squeeze;
76+
uniform float squeezeMin;
77+
uniform float squeezeMax;
78+
79+
// Colors
80+
uniform vec3 stroke;
81+
uniform vec3 backfaceStroke;
82+
uniform vec3 fill;
83+
84+
// This is like
85+
float wireframe_aastep(float threshold, float dist) {
86+
float afwidth = fwidth(dist) * 0.5;
87+
return smoothstep(threshold - afwidth, threshold + afwidth, dist);
88+
}
89+
90+
float wireframe_map(float value, float min1, float max1, float min2, float max2) {
91+
return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
92+
}
93+
94+
float getWireframe() {
95+
vec3 barycentric = v_edges_Barycentric;
96+
97+
// Distance from center of each triangle to its edges.
98+
float d = min(min(barycentric.x, barycentric.y), barycentric.z);
99+
100+
// for dashed rendering, we can use this to get the 0 .. 1 value of the line length
101+
float positionAlong = max(barycentric.x, barycentric.y);
102+
if (barycentric.y < barycentric.x && barycentric.y < barycentric.z) {
103+
positionAlong = 1.0 - positionAlong;
104+
}
105+
106+
// the thickness of the stroke
107+
float computedThickness = wireframe_map(thickness, 0.0, 1.0, 0.0, 0.34);
108+
109+
// if we want to shrink the thickness toward the center of the line segment
110+
if (squeeze) {
111+
computedThickness *= mix(squeezeMin, squeezeMax, (1.0 - sin(positionAlong * PI)));
112+
}
113+
114+
// Create dash pattern
115+
if (dash) {
116+
// here we offset the stroke position depending on whether it
117+
// should overlap or not
118+
float offset = 1.0 / dashRepeats * dashLength / 2.0;
119+
if (!dashInvert) {
120+
offset += 1.0 / dashRepeats / 2.0;
121+
}
122+
123+
// if we should animate the dash or not
124+
// if (dashAnimate) {
125+
// offset += time * 0.22;
126+
// }
127+
128+
// create the repeating dash pattern
129+
float pattern = fract((positionAlong + offset) * dashRepeats);
130+
computedThickness *= 1.0 - wireframe_aastep(dashLength, pattern);
131+
}
132+
133+
// compute the anti-aliased stroke edge
134+
float edge = 1.0 - wireframe_aastep(computedThickness, d);
135+
136+
return edge;
137+
}
138+
`,
139+
};
140+
141+
export const WireframeMaterial = shaderMaterial(
142+
WireframeMaterialShaders.uniforms,
143+
WireframeMaterialShaders.vertex +
144+
/* glsl */ `
145+
void main() {
146+
initWireframe();
147+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
148+
}
149+
`,
150+
WireframeMaterialShaders.fragment +
151+
/* glsl */ `
152+
void main () {
153+
// Compute color
154+
155+
float edge = getWireframe();
156+
vec4 colorStroke = vec4(stroke, edge);
157+
158+
#ifdef FLIP_SIDED
159+
colorStroke.rgb = backfaceStroke;
160+
#endif
161+
162+
vec4 colorFill = vec4(fill, fillOpacity);
163+
vec4 outColor = mix(colorFill, colorStroke, edge * strokeOpacity);
164+
165+
gl_FragColor = outColor;
166+
}
167+
`,
168+
);
169+
170+
declare global {
171+
interface HTMLElementTagNameMap {
172+
/**
173+
* @extends ngt-shader-material
174+
*/
175+
'ngt-wireframe-material': WireframeMaterialState & NgtShaderMaterial;
176+
}
177+
}
178+
179+
export function setWireframeOverride(
180+
material: THREE.Material,
181+
uniforms: {
182+
[key: string]: THREE.IUniform<any>;
183+
},
184+
) {
185+
material.onBeforeCompile = (shader) => {
186+
shader.uniforms = {
187+
...shader.uniforms,
188+
...uniforms,
189+
};
190+
191+
shader.vertexShader = shader.vertexShader.replace(
192+
'void main() {',
193+
`
194+
${WireframeMaterialShaders.vertex}
195+
void main() {
196+
initWireframe();
197+
`,
198+
);
199+
200+
shader.fragmentShader = shader.fragmentShader.replace(
201+
'void main() {',
202+
`
203+
${WireframeMaterialShaders.fragment}
204+
void main() {
205+
`,
206+
);
207+
208+
shader.fragmentShader = shader.fragmentShader.replace(
209+
'#include <color_fragment>',
210+
/* glsl */ `
211+
#include <color_fragment>
212+
float edge = getWireframe();
213+
vec4 colorStroke = vec4(stroke, edge);
214+
#ifdef FLIP_SIDED
215+
colorStroke.rgb = backfaceStroke;
216+
#endif
217+
vec4 colorFill = vec4(mix(diffuseColor.rgb, fill, fillMix), mix(diffuseColor.a, fillOpacity, fillMix));
218+
vec4 outColor = mix(colorFill, colorStroke, edge * strokeOpacity);
219+
220+
diffuseColor.rgb = outColor.rgb;
221+
diffuseColor.a *= outColor.a;
222+
`,
223+
);
224+
};
225+
226+
material.side = THREE.DoubleSide;
227+
material.transparent = true;
228+
}
229+
230+
export function injectNgtsWireframeUniforms(
231+
uniformsFactory: () => Record<string, THREE.IUniform<any>>,
232+
stateFactory: () => Partial<WireframeMaterialState>,
233+
{ injector }: { injector?: Injector } = {},
234+
) {
235+
injector = assertInjectionContext(injectNgtsWireframeUniforms, injector);
236+
return runInInjectionContext(injector, () => {
237+
const uniforms = uniformsFactory();
238+
const state = computed(() => stateFactory());
239+
const fillOpacity = computed(() => state().fillOpacity);
240+
const fillMix = computed(() => state().fillMix);
241+
const strokeOpacity = computed(() => state().strokeOpacity);
242+
const thickness = computed(() => state().thickness);
243+
const colorBackfaces = computed(() => state().colorBackfaces);
244+
const dash = computed(() => state().dash);
245+
const dashInvert = computed(() => state().dashInvert);
246+
const dashRepeats = computed(() => state().dashRepeats);
247+
const dashLength = computed(() => state().dashLength);
248+
const squeeze = computed(() => state().squeeze);
249+
const squeezeMin = computed(() => state().squeezeMin);
250+
const squeezeMax = computed(() => state().squeezeMax);
251+
const stroke = computed(() => state().stroke);
252+
const fill = computed(() => state().fill);
253+
const backfaceStroke = computed(() => state().backfaceStroke);
254+
255+
effect(() => {
256+
uniforms['fillOpacity'].value = fillOpacity() ?? uniforms['fillOpacity'].value;
257+
});
258+
259+
effect(() => {
260+
uniforms['fillMix'].value = fillMix() ?? uniforms['fillMix'].value;
261+
});
262+
263+
effect(() => {
264+
uniforms['strokeOpacity'].value = strokeOpacity() ?? uniforms['strokeOpacity'].value;
265+
});
266+
267+
effect(() => {
268+
uniforms['thickness'].value = thickness() ?? uniforms['thickness'].value;
269+
});
270+
271+
effect(() => {
272+
uniforms['colorBackfaces'].value = colorBackfaces() ?? uniforms['colorBackfaces'].value;
273+
});
274+
275+
effect(() => {
276+
uniforms['dash'].value = dash() ?? uniforms['dash'].value;
277+
});
278+
279+
effect(() => {
280+
uniforms['dashInvert'].value = dashInvert() ?? uniforms['dashInvert'].value;
281+
});
282+
283+
effect(() => {
284+
uniforms['dashRepeats'].value = dashRepeats() ?? uniforms['dashRepeats'].value;
285+
});
286+
287+
effect(() => {
288+
uniforms['dashLength'].value = dashLength() ?? uniforms['dashLength'].value;
289+
});
290+
291+
effect(() => {
292+
uniforms['squeeze'].value = squeeze() ?? uniforms['squeeze'].value;
293+
});
294+
295+
effect(() => {
296+
uniforms['squeezeMin'].value = squeezeMin() ?? uniforms['squeezeMin'].value;
297+
});
298+
299+
effect(() => {
300+
uniforms['squeezeMax'].value = squeezeMax() ?? uniforms['squeezeMax'].value;
301+
});
302+
303+
effect(() => {
304+
uniforms['stroke'].value = stroke() ? new THREE.Color(stroke()) : uniforms['stroke'].value;
305+
});
306+
307+
effect(() => {
308+
uniforms['fill'].value = fill() ? new THREE.Color(fill()) : uniforms['fill'].value;
309+
});
310+
311+
effect(() => {
312+
uniforms['backfaceStroke'].value = backfaceStroke()
313+
? new THREE.Color(backfaceStroke())
314+
: uniforms['backfaceStroke'].value;
315+
});
316+
});
317+
}
+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { CUSTOM_ELEMENTS_SCHEMA, Component } from '@angular/core';
2+
import { NgtArgs } from 'angular-three';
3+
import { NgtsEnvironment, NgtsWireframe } from 'angular-three-soba/staging';
4+
import { IcosahedronGeometry } from 'three';
5+
import { makeCanvasOptions, makeDecorators, makeStoryFunction } from '../setup-canvas';
6+
7+
@Component({
8+
standalone: true,
9+
template: `
10+
<ngt-mesh>
11+
<ngt-icosahedron-geometry *args="[1, 16]" />
12+
<ngt-mesh-physical-material color="red" [roughness]="0.2" [metalness]="1" />
13+
<ngts-wireframe stroke="white" [squeeze]="true" [dash]="true" />
14+
</ngt-mesh>
15+
16+
<ngt-mesh [position]="[0, 0, -2.5]">
17+
<ngt-torus-knot-geometry />
18+
<ngt-mesh-basic-material color="red" />
19+
<ngts-wireframe
20+
[simplify]="true"
21+
stroke="white"
22+
[squeeze]="true"
23+
[dash]="true"
24+
[fillMix]="1"
25+
[fillOpacity]="0.2"
26+
/>
27+
</ngt-mesh>
28+
29+
<ngt-group [position]="[-2.5, 0, -2.5]">
30+
<ngts-wireframe
31+
[fill]="blue"
32+
[geometry]="geometry"
33+
stroke="white"
34+
[squeeze]="true"
35+
[dash]="true"
36+
[fillMix]="1"
37+
[fillOpacity]="0.2"
38+
/>
39+
</ngt-group>
40+
41+
<ngt-mesh [position]="[-2.5, 0, 0]">
42+
<ngt-sphere-geometry *args="[1, 16, 16]" />
43+
<ngt-shader-material [vertexShader]="vertexShader" [fragmentShader]="fragmentShader" />
44+
<ngts-wireframe stroke="white" [squeeze]="true" [dash]="true" />
45+
</ngt-mesh>
46+
47+
<ngts-environment [background]="true" preset="sunset" [blur]="0.2" />
48+
`,
49+
imports: [NgtsWireframe, NgtsEnvironment, NgtArgs],
50+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
51+
})
52+
class DefaultWireframeStory {
53+
geometry = new IcosahedronGeometry(1, 16);
54+
vertexShader = /* glsl */ `
55+
void main() {
56+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
57+
}
58+
`;
59+
fragmentShader = /* glsl */ `
60+
void main() {
61+
float edge = getWireframe();
62+
gl_FragColor = vec4(1.0, 1.0, 0.0, edge);
63+
}
64+
`;
65+
}
66+
67+
export default {
68+
title: 'Staging/Wireframe',
69+
decorators: makeDecorators(),
70+
};
71+
72+
const canvasOptions = makeCanvasOptions({ camera: { position: [2, 2, 2] } });
73+
74+
export const Default = makeStoryFunction(DefaultWireframeStory, canvasOptions);

0 commit comments

Comments
 (0)