Skip to content

Commit c6f4fdd

Browse files
committed
docs: apply maxime article on particles
1 parent 02cc8aa commit c6f4fdd

File tree

10 files changed

+415
-2
lines changed

10 files changed

+415
-2
lines changed

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@ const routes: Routes = [
3737
},
3838
},
3939
},
40+
{
41+
path: 'particle-maxime',
42+
loadComponent: () => import('./particle-maxime/particle-maxime'),
43+
data: {
44+
credits: {
45+
title: 'The magical world of particles with React Three Fiber and shaders',
46+
link: 'https://blog.maximeheckel.com/posts/the-magical-world-of-particles-with-react-three-fiber-and-shaders/',
47+
class: 'text-white',
48+
},
49+
},
50+
},
4051
{
4152
path: '',
4253
redirectTo: 'basic',
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
void main() {
2+
vec3 color = vec3(0.34, 0.53, 0.96);
3+
gl_FragColor = vec4(color, 1.0);
4+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { ChangeDetectionStrategy, Component } from '@angular/core';
2+
import { NgtCanvas } from 'angular-three/dom';
3+
import { SceneGraph } from './scene';
4+
5+
@Component({
6+
template: `
7+
<ngt-canvas>
8+
<app-scene-graph *canvasContent />
9+
</ngt-canvas>
10+
`,
11+
imports: [NgtCanvas, SceneGraph],
12+
changeDetection: ChangeDetectionStrategy.OnPush,
13+
host: { class: 'particle-maxime' },
14+
})
15+
export default class ParticleMaxime {}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, viewChild } from '@angular/core';
2+
import { extend, injectBeforeRender, NgtArgs, NgtPortal } from 'angular-three';
3+
import { NgtsPerspectiveCamera } from 'angular-three-soba/cameras';
4+
import { NgtsOrbitControls } from 'angular-three-soba/controls';
5+
import { injectFBO } from 'angular-three-soba/misc';
6+
import * as THREE from 'three';
7+
8+
import { SimulationMaterial } from './simulation-material';
9+
10+
import fragmentShader from './fragment.glsl' with { loader: 'text' };
11+
import vertexShader from './vertex.glsl' with { loader: 'text' };
12+
13+
extend({ SimulationMaterial });
14+
15+
@Component({
16+
selector: 'app-fbo-particles',
17+
template: `
18+
<ngt-portal [container]="virtualScene">
19+
<ng-template portalContent>
20+
<ngt-mesh>
21+
<ngt-simulation-material #simulationMaterial *args="[size]" />
22+
<ngt-buffer-geometry>
23+
<ngt-buffer-attribute
24+
attach="attributes.position"
25+
[count]="positions.length / 3"
26+
[array]="positions"
27+
[itemSize]="3"
28+
/>
29+
<ngt-buffer-attribute
30+
attach="attributes.uv"
31+
[count]="uvs.length / 2"
32+
[array]="uvs"
33+
[itemSize]="2"
34+
/>
35+
</ngt-buffer-geometry>
36+
</ngt-mesh>
37+
</ng-template>
38+
</ngt-portal>
39+
40+
<ngt-points>
41+
<ngt-buffer-geometry>
42+
<ngt-buffer-attribute
43+
attach="attributes.position"
44+
[count]="particlePositions.length / 3"
45+
[array]="particlePositions"
46+
[itemSize]="3"
47+
/>
48+
</ngt-buffer-geometry>
49+
<ngt-shader-material
50+
[depthWrite]="false"
51+
[uniforms]="uniforms"
52+
[vertexShader]="vertexShader"
53+
[fragmentShader]="fragmentShader"
54+
[blending]="AdditiveBlending"
55+
/>
56+
</ngt-points>
57+
`,
58+
imports: [NgtArgs, NgtPortal],
59+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
60+
changeDetection: ChangeDetectionStrategy.OnPush,
61+
})
62+
export class FBOParticles {
63+
protected readonly Math = Math;
64+
protected readonly vertexShader = vertexShader;
65+
protected readonly fragmentShader = fragmentShader;
66+
protected readonly AdditiveBlending = THREE.AdditiveBlending;
67+
68+
protected size = 128;
69+
protected virtualScene = new THREE.Scene();
70+
protected virtualCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 1 / Math.pow(2, 53), 1);
71+
protected positions = new Float32Array([-1, -1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, 1, 1, 0, -1, 1, 0]);
72+
protected uvs = new Float32Array([
73+
0,
74+
0, // bottom-left
75+
1,
76+
0, // bottom-right
77+
1,
78+
1, // top-right
79+
0,
80+
0, // bottom-left
81+
1,
82+
1, // top-right
83+
0,
84+
1, // top-left
85+
]);
86+
protected renderTarget = injectFBO(() => ({
87+
width: this.size,
88+
height: this.size,
89+
settings: {
90+
minFilter: THREE.NearestFilter,
91+
magFilter: THREE.NearestFilter,
92+
format: THREE.RGBAFormat,
93+
stencilBuffer: false,
94+
type: THREE.FloatType,
95+
},
96+
}));
97+
98+
protected particlePositions = (() => {
99+
const length = this.size * this.size;
100+
const particles = new Float32Array(length * 3);
101+
for (let i = 0; i < length; i++) {
102+
let i3 = i * 3;
103+
particles[i3 + 0] = (i % this.size) / this.size;
104+
particles[i3 + 1] = i / this.size / this.size;
105+
}
106+
return particles;
107+
})();
108+
109+
protected uniforms: Record<string, THREE.IUniform> = {
110+
uPositions: { value: null },
111+
};
112+
113+
private simulationMaterialRef = viewChild<ElementRef<SimulationMaterial>>('simulationMaterial');
114+
115+
constructor() {
116+
injectBeforeRender(({ gl, clock }) => {
117+
gl.setRenderTarget(this.renderTarget());
118+
gl.clear();
119+
gl.render(this.virtualScene, this.virtualCamera);
120+
gl.setRenderTarget(null);
121+
122+
const simulationMateria = this.simulationMaterialRef()?.nativeElement;
123+
124+
if (!simulationMateria) return;
125+
126+
this.uniforms['uPositions'].value = this.renderTarget().texture;
127+
simulationMateria.uniforms['uTime'].value = clock.elapsedTime;
128+
});
129+
}
130+
}
131+
132+
@Component({
133+
selector: 'app-scene-graph',
134+
template: `
135+
<ngts-perspective-camera [options]="{ makeDefault: true, position: [1.5, 1.5, 2.5], fov: 75 }" />
136+
137+
<ngt-color *args="['#20222B']" attach="background" />
138+
<ngt-ambient-light [intensity]="Math.PI * 0.5" />
139+
140+
<app-fbo-particles />
141+
142+
<ngts-orbit-controls [options]="{ autoRotate: true }" />
143+
`,
144+
imports: [NgtsPerspectiveCamera, NgtsOrbitControls, NgtArgs, FBOParticles],
145+
changeDetection: ChangeDetectionStrategy.OnPush,
146+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
147+
})
148+
export class SceneGraph {
149+
protected readonly Math = Math;
150+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
uniform sampler2D positions;
2+
uniform float uTime;
3+
uniform float uFrequency;
4+
5+
varying vec2 vUv;
6+
7+
// Source: https://github.com/drcmda/glsl-curl-noise2
8+
// and: https://github.com/guoweish/glsl-noise-simplex/blob/master/3d.glsl
9+
10+
//
11+
// Description : Array and textureless GLSL 2D/3D/4D simplex
12+
// noise functions.
13+
// Author : Ian McEwan, Ashima Arts.
14+
// Maintainer : ijm
15+
// Lastmod : 20110822 (ijm)
16+
// License : Copyright (C) 2011 Ashima Arts. All rights reserved.
17+
// Distributed under the MIT License. See LICENSE file.
18+
// https://github.com/ashima/webgl-noise
19+
//
20+
21+
vec3 mod289(vec3 x) {
22+
return x - floor(x * (1.0 / 289.0)) * 289.0;
23+
}
24+
25+
vec4 mod289(vec4 x) {
26+
return x - floor(x * (1.0 / 289.0)) * 289.0;
27+
}
28+
29+
vec4 permute(vec4 x) {
30+
return mod289(((x * 34.0) + 1.0) * x);
31+
}
32+
33+
vec4 taylorInvSqrt(vec4 r)
34+
{
35+
return 1.79284291400159 - 0.85373472095314 * r;
36+
}
37+
38+
float snoise(vec3 v)
39+
{
40+
const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0);
41+
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
42+
43+
// First corner
44+
vec3 i = floor(v + dot(v, C.yyy));
45+
vec3 x0 = v - i + dot(i, C.xxx);
46+
47+
// Other corners
48+
vec3 g = step(x0.yzx, x0.xyz);
49+
vec3 l = 1.0 - g;
50+
vec3 i1 = min(g.xyz, l.zxy);
51+
vec3 i2 = max(g.xyz, l.zxy);
52+
53+
// x0 = x0 - 0.0 + 0.0 * C.xxx;
54+
// x1 = x0 - i1 + 1.0 * C.xxx;
55+
// x2 = x0 - i2 + 2.0 * C.xxx;
56+
// x3 = x0 - 1.0 + 3.0 * C.xxx;
57+
vec3 x1 = x0 - i1 + C.xxx;
58+
vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y
59+
vec3 x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y
60+
61+
// Permutations
62+
i = mod289(i);
63+
vec4 p = permute(permute(permute(
64+
i.z + vec4(0.0, i1.z, i2.z, 1.0))
65+
+ i.y + vec4(0.0, i1.y, i2.y, 1.0))
66+
+ i.x + vec4(0.0, i1.x, i2.x, 1.0));
67+
68+
// Gradients: 7x7 points over a square, mapped onto an octahedron.
69+
// The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294)
70+
float n_ = 0.142857142857; // 1.0/7.0
71+
vec3 ns = n_ * D.wyz - D.xzx;
72+
73+
vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7)
74+
75+
vec4 x_ = floor(j * ns.z);
76+
vec4 y_ = floor(j - 7.0 * x_); // mod(j,N)
77+
78+
vec4 x = x_ * ns.x + ns.yyyy;
79+
vec4 y = y_ * ns.x + ns.yyyy;
80+
vec4 h = 1.0 - abs(x) - abs(y);
81+
82+
vec4 b0 = vec4(x.xy, y.xy);
83+
vec4 b1 = vec4(x.zw, y.zw);
84+
85+
//vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0;
86+
//vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0;
87+
vec4 s0 = floor(b0) * 2.0 + 1.0;
88+
vec4 s1 = floor(b1) * 2.0 + 1.0;
89+
vec4 sh = -step(h, vec4(0.0));
90+
91+
vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
92+
vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
93+
94+
vec3 p0 = vec3(a0.xy, h.x);
95+
vec3 p1 = vec3(a0.zw, h.y);
96+
vec3 p2 = vec3(a1.xy, h.z);
97+
vec3 p3 = vec3(a1.zw, h.w);
98+
99+
//Normalise gradients
100+
vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));
101+
p0 *= norm.x;
102+
p1 *= norm.y;
103+
p2 *= norm.z;
104+
p3 *= norm.w;
105+
106+
// Mix final noise value
107+
vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
108+
m = m * m;
109+
return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1),
110+
dot(p2, x2), dot(p3, x3)));
111+
}
112+
113+
vec3 snoiseVec3(vec3 x) {
114+
float s = snoise(vec3(x));
115+
float s1 = snoise(vec3(x.y - 19.1, x.z + 33.4, x.x + 47.2));
116+
float s2 = snoise(vec3(x.z + 74.2, x.x - 124.5, x.y + 99.4));
117+
vec3 c = vec3(s, s1, s2);
118+
return c;
119+
}
120+
121+
vec3 curlNoise(vec3 p) {
122+
const float e = .1;
123+
vec3 dx = vec3(e, 0.0, 0.0);
124+
vec3 dy = vec3(0.0, e, 0.0);
125+
vec3 dz = vec3(0.0, 0.0, e);
126+
127+
vec3 p_x0 = snoiseVec3(p - dx);
128+
vec3 p_x1 = snoiseVec3(p + dx);
129+
vec3 p_y0 = snoiseVec3(p - dy);
130+
vec3 p_y1 = snoiseVec3(p + dy);
131+
vec3 p_z0 = snoiseVec3(p - dz);
132+
vec3 p_z1 = snoiseVec3(p + dz);
133+
134+
float x = p_y1.z - p_y0.z - p_z1.y + p_z0.y;
135+
float y = p_z1.x - p_z0.x - p_x1.z + p_x0.z;
136+
float z = p_x1.y - p_x0.y - p_y1.x + p_y0.x;
137+
138+
const float divisor = 1.0 / (2.0 * e);
139+
return normalize(vec3(x, y, z) * divisor);
140+
}
141+
142+
void main() {
143+
vec3 pos = texture2D(positions, vUv).rgb;
144+
vec3 curlPos = texture2D(positions, vUv).rgb;
145+
146+
pos = curlNoise(pos * uFrequency + uTime * 0.1);
147+
curlPos = curlNoise(curlPos * uFrequency + uTime * 0.1);
148+
curlPos += curlNoise(curlPos * uFrequency * 2.0) * 0.5;
149+
150+
gl_FragColor = vec4(mix(pos, curlPos, sin(uTime)), 1.0);
151+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import simulationFragmentShader from './simulation-fragment.glsl' with { loader: 'text' };
2+
import simulationVertexShader from './simulation-vertex.glsl' with { loader: 'text' };
3+
4+
import * as THREE from 'three';
5+
6+
function getRandomData(width: number, height: number) {
7+
// we need to create a vec4 since we're passing the positions to the fragment shader
8+
// data textures need to have 4 components, R, G, B, and A
9+
const length = width * height * 4;
10+
const data = new Float32Array(length);
11+
12+
for (let i = 0; i < length; i++) {
13+
const stride = i * 4;
14+
15+
const distance = Math.sqrt(Math.random()) * 2.0;
16+
const theta = THREE.MathUtils.randFloatSpread(360);
17+
const phi = THREE.MathUtils.randFloatSpread(360);
18+
19+
data[stride] = distance * Math.sin(theta) * Math.cos(phi);
20+
data[stride + 1] = distance * Math.sin(theta) * Math.sin(phi);
21+
data[stride + 2] = distance * Math.cos(theta);
22+
data[stride + 3] = 1.0; // this value will not have any impact
23+
}
24+
25+
return data;
26+
}
27+
28+
export class SimulationMaterial extends THREE.ShaderMaterial {
29+
constructor(size: number) {
30+
const positionsTexture = new THREE.DataTexture(
31+
getRandomData(size, size),
32+
size,
33+
size,
34+
THREE.RGBAFormat,
35+
THREE.FloatType,
36+
);
37+
positionsTexture.needsUpdate = true;
38+
39+
const simulationUniforms = {
40+
positions: { value: positionsTexture },
41+
uFrequency: { value: 0.25 },
42+
uTime: { value: 0 },
43+
};
44+
45+
super({
46+
uniforms: simulationUniforms,
47+
vertexShader: simulationVertexShader,
48+
fragmentShader: simulationFragmentShader,
49+
});
50+
}
51+
}

0 commit comments

Comments
 (0)