Skip to content

Commit 343fd8f

Browse files
Chau TranChau Tran
Chau Tran
authored and
Chau Tran
committed
docs: migrate height field
1 parent 774c70f commit 343fd8f

File tree

5 files changed

+267
-0
lines changed

5 files changed

+267
-0
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<ngt-instanced-mesh
2+
*args="[undefined, undefined, number()]"
3+
[ref]="sphereBody.ref"
4+
[castShadow]="true"
5+
[receiveShadow]="true"
6+
>
7+
<ngt-sphere-geometry *args="[0.2, 16, 16]">
8+
<ngt-instanced-buffer-attribute attach="attributes.color" *args="[colors(), 3]" />
9+
</ngt-sphere-geometry>
10+
<ngt-mesh-phong-material [vertexColors]="true" />
11+
</ngt-instanced-mesh>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<ngt-mesh [ref]="fieldBody.ref" [castShadow]="true" [receiveShadow]="true">
2+
<ngt-mesh-phong-material [color]="color" />
3+
<height-map-geometry [elementSize]="fieldElementSize()" [heights]="fieldHeights()" />
4+
</ngt-mesh>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<ngt-canvas [sceneGraph]="scene" [shadows]="true" [camera]="{position: [0, -10, 10]}" />
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import { RouteMeta } from '@analogjs/router';
2+
import { Component, computed, CUSTOM_ELEMENTS_SCHEMA, effect, ElementRef, Input, ViewChild } from '@angular/core';
3+
import { Triplet } from '@pmndrs/cannon-worker-api';
4+
import { NgtArgs, NgtCanvas, NgtSignalStore } from 'angular-three';
5+
import { NgtcPhysics } from 'angular-three-cannon';
6+
import { injectHeightfield, injectSphere } from 'angular-three-cannon/services';
7+
import { NgtsOrbitControls } from 'angular-three-soba/controls';
8+
// @ts-ignore
9+
import niceColors from 'nice-color-palettes';
10+
import * as THREE from 'three';
11+
12+
export const routeMeta: RouteMeta = {
13+
title: 'Height Field',
14+
};
15+
16+
type GenerateHeightmapArgs = {
17+
height: number;
18+
number: number;
19+
scale: number;
20+
width: number;
21+
};
22+
23+
/* Generates a 2D array using Worley noise. */
24+
function generateHeightmap({ width, height, number, scale }: GenerateHeightmapArgs) {
25+
const data = [];
26+
27+
const seedPoints = [];
28+
for (let i = 0; i < number; i++) {
29+
seedPoints.push([Math.random(), Math.random()]);
30+
}
31+
32+
let max = 0;
33+
for (let i = 0; i < width; i++) {
34+
const row = [];
35+
for (let j = 0; j < height; j++) {
36+
let min = Infinity;
37+
seedPoints.forEach((p) => {
38+
const distance2 = (p[0] - i / width) ** 2 + (p[1] - j / height) ** 2;
39+
if (distance2 < min) {
40+
min = distance2;
41+
}
42+
});
43+
const d = Math.sqrt(min);
44+
if (d > max) {
45+
max = d;
46+
}
47+
row.push(d);
48+
}
49+
data.push(row);
50+
}
51+
52+
/* Normalize and scale. */
53+
for (let i = 0; i < width; i++) {
54+
for (let j = 0; j < height; j++) {
55+
data[i][j] *= scale / max;
56+
}
57+
}
58+
return data;
59+
}
60+
61+
@Component({
62+
selector: 'height-field-spheres',
63+
standalone: true,
64+
templateUrl: 'height-field-spheres.html',
65+
imports: [NgtArgs],
66+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
67+
})
68+
class HeightFieldSpheres extends NgtSignalStore<{ columns: number; rows: number; spread: number }> {
69+
@Input() set columns(columns: number) {
70+
this.set({ columns });
71+
}
72+
73+
@Input() set rows(rows: number) {
74+
this.set({ rows });
75+
}
76+
77+
@Input() set spread(spread: number) {
78+
this.set({ spread });
79+
}
80+
81+
readonly #columns = this.select('columns');
82+
readonly #rows = this.select('rows');
83+
readonly #spread = this.select('spread');
84+
85+
readonly sphereBody = injectSphere<THREE.InstancedMesh>((index) => ({
86+
args: [0.2],
87+
mass: 1,
88+
position: [
89+
((index % this.#columns()) - (this.#columns() - 1) / 2) * this.#spread(),
90+
2.0,
91+
(Math.floor(index / this.#columns()) - (this.#rows() - 1) / 2) * this.#spread(),
92+
],
93+
}));
94+
95+
readonly number = computed(() => this.#columns() * this.#rows());
96+
readonly colors = computed(() => new Float32Array(this.number() * 3));
97+
98+
constructor() {
99+
super({ columns: 0, rows: 0, spread: 0 });
100+
effect(() => {
101+
const colors = this.colors();
102+
const number = this.number();
103+
const color = new THREE.Color();
104+
105+
for (let i = 0; i < number; i++) {
106+
color
107+
.set(niceColors[17][Math.floor(Math.random() * 5)])
108+
.convertSRGBToLinear()
109+
.toArray(colors, i * 3);
110+
}
111+
});
112+
}
113+
}
114+
115+
@Component({
116+
selector: 'height-map-geometry',
117+
standalone: true,
118+
template: ` <ngt-buffer-geometry #geometry /> `,
119+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
120+
})
121+
class HeightMapGeometry extends NgtSignalStore<{
122+
heights: number[][];
123+
elementSize: number;
124+
geometry: THREE.BufferGeometry;
125+
}> {
126+
@Input() set elementSize(elementSize: number) {
127+
this.set({ elementSize });
128+
}
129+
130+
@Input() set heights(heights: number[][]) {
131+
this.set({ heights });
132+
}
133+
134+
@ViewChild('geometry', { static: true }) set geometry(geometry: ElementRef<THREE.BufferGeometry>) {
135+
this.set({ geometry: geometry.nativeElement });
136+
}
137+
138+
readonly #heights = this.select('heights');
139+
readonly #geometry = this.select('geometry');
140+
141+
constructor() {
142+
super();
143+
effect(() => {
144+
const heights = this.#heights();
145+
const geometry = this.#geometry();
146+
if (!heights || !geometry) return;
147+
148+
const elementSize = this.get('elementSize');
149+
150+
const dx = elementSize;
151+
const dy = elementSize;
152+
153+
/* create the vertex data from heights */
154+
const vertices = heights.flatMap((row, i) => row.flatMap((z, j) => [i * dx, j * dy, z]));
155+
156+
/* create the faces */
157+
const indices = [];
158+
for (let i = 0; i < heights.length - 1; i++) {
159+
for (let j = 0; j < heights[i].length - 1; j++) {
160+
const stride = heights[i].length;
161+
const index = i * stride + j;
162+
indices.push(index + 1, index + stride, index + stride + 1);
163+
indices.push(index + stride, index + 1, index);
164+
}
165+
}
166+
167+
geometry.setIndex(indices);
168+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
169+
geometry.computeVertexNormals();
170+
geometry.computeBoundingBox();
171+
geometry.computeBoundingSphere();
172+
});
173+
}
174+
}
175+
176+
@Component({
177+
selector: 'height-field',
178+
standalone: true,
179+
templateUrl: 'height-field.html',
180+
imports: [HeightMapGeometry],
181+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
182+
})
183+
class Field extends NgtSignalStore<{ heights: number[][]; elementSize: number; position: Triplet; rotation: Triplet }> {
184+
@Input() set elementSize(elementSize: number) {
185+
this.set({ elementSize });
186+
}
187+
188+
@Input() set heights(heights: number[][]) {
189+
this.set({ heights });
190+
}
191+
192+
@Input() set position(position: Triplet) {
193+
this.set({ position });
194+
}
195+
196+
@Input() set rotation(rotation: Triplet) {
197+
this.set({ rotation });
198+
}
199+
200+
readonly color = niceColors[17][4];
201+
202+
readonly fieldHeights = this.select('heights');
203+
readonly fieldElementSize = this.select('elementSize');
204+
readonly #position = this.select('position');
205+
readonly #rotation = this.select('rotation');
206+
207+
readonly fieldBody = injectHeightfield<THREE.Mesh>(() => ({
208+
args: [this.fieldHeights(), { elementSize: this.fieldElementSize() }],
209+
position: this.#position(),
210+
rotation: this.#rotation(),
211+
}));
212+
213+
constructor() {
214+
super({ elementSize: 0, position: [0, 0, 0], rotation: [0, 0, 0], heights: [] });
215+
}
216+
}
217+
218+
@Component({
219+
standalone: true,
220+
templateUrl: 'scene.html',
221+
imports: [NgtsOrbitControls, NgtcPhysics, Field, HeightFieldSpheres, NgtArgs],
222+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
223+
})
224+
class SceneGraph {
225+
readonly Math = Math;
226+
readonly scale = 10;
227+
readonly heights = generateHeightmap({ height: 128, width: 128, number: 10, scale: 1 });
228+
}
229+
230+
@Component({
231+
standalone: true,
232+
templateUrl: 'index.html',
233+
imports: [NgtCanvas],
234+
})
235+
export default class HeightField {
236+
readonly scene = SceneGraph;
237+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<ngt-color *args="['#171720']" attach="background" />
2+
<ngts-orbit-controls [dampingFactor]="0.2" [minPolarAngle]="Math.PI / 3" [maxPolarAngle]="Math.PI / 3" />
3+
4+
<ngtc-physics>
5+
<ngt-ambient-light [intensity]="0.5" />
6+
<ngt-directional-light [position]="[0, 3, 0]" [castShadow]="true" />
7+
<height-field
8+
[elementSize]="scale / 128"
9+
[heights]="heights"
10+
[position]="[-scale / 2, 0, scale / 2]"
11+
[rotation]="[Math.PI / -2, 0, 0]"
12+
/>
13+
<height-field-spheres [rows]="3" [columns]="3" [spread]="4" />
14+
</ngtc-physics>

0 commit comments

Comments
 (0)