@jgengine/shell 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +27 -0
- package/dist/GamePlayerShell.js +19 -9
- package/dist/demo/demoGame.js +10 -2
- package/dist/demo/environmentShowcase.d.ts +2 -0
- package/dist/demo/environmentShowcase.js +15 -0
- package/dist/environment/EnvironmentScene.d.ts +5 -0
- package/dist/environment/EnvironmentScene.js +75 -0
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +1 -0
- package/dist/environment.d.ts +1 -0
- package/dist/environment.js +1 -0
- package/dist/registry.d.ts +4 -2
- package/dist/structures/GeneratedBuilding.d.ts +44 -0
- package/dist/structures/GeneratedBuilding.js +86 -0
- package/dist/terrain/GrassField.d.ts +21 -0
- package/dist/terrain/GrassField.js +31 -0
- package/dist/terrain/ProceduralGround.d.ts +9 -0
- package/dist/terrain/ProceduralGround.js +17 -0
- package/dist/terrain/grassGeometry.d.ts +27 -0
- package/dist/terrain/grassGeometry.js +77 -0
- package/dist/terrain/grassMaterial.d.ts +33 -0
- package/dist/terrain/grassMaterial.js +108 -0
- package/dist/terrain/index.d.ts +7 -0
- package/dist/terrain/index.js +7 -0
- package/dist/terrain/random.d.ts +4 -0
- package/dist/terrain/random.js +27 -0
- package/dist/terrain/terrainMath.d.ts +34 -0
- package/dist/terrain/terrainMath.js +92 -0
- package/dist/terrain.d.ts +1 -0
- package/dist/terrain.js +1 -0
- package/dist/water/Ocean.d.ts +6 -0
- package/dist/water/Ocean.js +30 -0
- package/dist/water/OceanConfig.d.ts +90 -0
- package/dist/water/OceanConfig.js +131 -0
- package/dist/water/OceanMaterial.d.ts +54 -0
- package/dist/water/OceanMaterial.js +51 -0
- package/dist/water/OceanShader.d.ts +2 -0
- package/dist/water/OceanShader.js +78 -0
- package/dist/water/index.d.ts +3 -0
- package/dist/water/index.js +3 -0
- package/dist/water.d.ts +1 -0
- package/dist/water.js +1 -0
- package/dist/weather/LightningStrike.d.ts +17 -0
- package/dist/weather/LightningStrike.js +130 -0
- package/dist/weather/RainField.d.ts +20 -0
- package/dist/weather/RainField.js +107 -0
- package/dist/weather/SnowField.d.ts +19 -0
- package/dist/weather/SnowField.js +108 -0
- package/dist/weather/WeatherLayer.d.ts +17 -0
- package/dist/weather/WeatherLayer.js +16 -0
- package/dist/weather/index.d.ts +5 -0
- package/dist/weather/index.js +5 -0
- package/dist/weather/weatherGeometry.d.ts +2 -0
- package/dist/weather/weatherGeometry.js +14 -0
- package/dist/weather/weatherMath.d.ts +7 -0
- package/dist/weather/weatherMath.js +30 -0
- package/dist/weather/weatherUniforms.d.ts +18 -0
- package/dist/weather/weatherUniforms.js +34 -0
- package/dist/world/InstancedBodies.d.ts +17 -0
- package/dist/world/InstancedBodies.js +84 -0
- package/package.json +8 -4
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { createSeededRandom } from "./random.js";
|
|
3
|
+
import { resolveTerrainSize } from "./terrainMath.js";
|
|
4
|
+
export function resolveGrassRange(value, fallback) {
|
|
5
|
+
if (value === undefined)
|
|
6
|
+
return fallback;
|
|
7
|
+
return typeof value === "number" ? [value, value] : value;
|
|
8
|
+
}
|
|
9
|
+
export function resolveGrassBladeGeometryOptions(options = {}) {
|
|
10
|
+
return {
|
|
11
|
+
count: Math.max(0, Math.floor(options.count ?? 6000)),
|
|
12
|
+
area: options.area ?? 40,
|
|
13
|
+
seed: options.seed ?? 1,
|
|
14
|
+
segments: Math.max(1, Math.floor(options.segments ?? 4)),
|
|
15
|
+
height: resolveGrassRange(options.height, [0.55, 1.1]),
|
|
16
|
+
width: resolveGrassRange(options.width, [0.035, 0.075]),
|
|
17
|
+
bend: resolveGrassRange(options.bend, [0.08, 0.32]),
|
|
18
|
+
heightAt: options.heightAt ?? (() => 0),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function randomRange(random, range) {
|
|
22
|
+
return THREE.MathUtils.lerp(range[0], range[1], random());
|
|
23
|
+
}
|
|
24
|
+
export function createGrassBladeGeometry(options = {}) {
|
|
25
|
+
const resolved = resolveGrassBladeGeometryOptions(options);
|
|
26
|
+
const size = resolveTerrainSize(resolved.area);
|
|
27
|
+
const positions = [];
|
|
28
|
+
const normals = [];
|
|
29
|
+
const uvs = [];
|
|
30
|
+
const indices = [];
|
|
31
|
+
for (let segment = 0; segment <= resolved.segments; segment += 1) {
|
|
32
|
+
const t = segment / resolved.segments;
|
|
33
|
+
positions.push(-0.5, t, 0, 0.5, t, 0);
|
|
34
|
+
normals.push(0, 0, 1, 0, 0, 1);
|
|
35
|
+
uvs.push(0, t, 1, t);
|
|
36
|
+
if (segment < resolved.segments) {
|
|
37
|
+
const base = segment * 2;
|
|
38
|
+
indices.push(base, base + 1, base + 3, base, base + 3, base + 2);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const geometry = new THREE.InstancedBufferGeometry();
|
|
42
|
+
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
|
|
43
|
+
geometry.setAttribute("normal", new THREE.Float32BufferAttribute(normals, 3));
|
|
44
|
+
geometry.setAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2));
|
|
45
|
+
geometry.setIndex(indices);
|
|
46
|
+
const random = createSeededRandom(resolved.seed);
|
|
47
|
+
const offsets = new Float32Array(resolved.count * 3);
|
|
48
|
+
const yaw = new Float32Array(resolved.count);
|
|
49
|
+
const heights = new Float32Array(resolved.count);
|
|
50
|
+
const widths = new Float32Array(resolved.count);
|
|
51
|
+
const bend = new Float32Array(resolved.count);
|
|
52
|
+
const phase = new Float32Array(resolved.count);
|
|
53
|
+
const colorMix = new Float32Array(resolved.count);
|
|
54
|
+
for (let index = 0; index < resolved.count; index += 1) {
|
|
55
|
+
const x = (random() - 0.5) * size.width;
|
|
56
|
+
const z = (random() - 0.5) * size.depth;
|
|
57
|
+
offsets[index * 3] = x;
|
|
58
|
+
offsets[index * 3 + 1] = resolved.heightAt(x, z);
|
|
59
|
+
offsets[index * 3 + 2] = z;
|
|
60
|
+
yaw[index] = random() * Math.PI * 2;
|
|
61
|
+
heights[index] = randomRange(random, resolved.height);
|
|
62
|
+
widths[index] = randomRange(random, resolved.width);
|
|
63
|
+
bend[index] = randomRange(random, resolved.bend);
|
|
64
|
+
phase[index] = random() * Math.PI * 2;
|
|
65
|
+
colorMix[index] = random();
|
|
66
|
+
}
|
|
67
|
+
geometry.setAttribute("instanceOffset", new THREE.InstancedBufferAttribute(offsets, 3));
|
|
68
|
+
geometry.setAttribute("instanceYaw", new THREE.InstancedBufferAttribute(yaw, 1));
|
|
69
|
+
geometry.setAttribute("instanceHeight", new THREE.InstancedBufferAttribute(heights, 1));
|
|
70
|
+
geometry.setAttribute("instanceWidth", new THREE.InstancedBufferAttribute(widths, 1));
|
|
71
|
+
geometry.setAttribute("instanceBend", new THREE.InstancedBufferAttribute(bend, 1));
|
|
72
|
+
geometry.setAttribute("instancePhase", new THREE.InstancedBufferAttribute(phase, 1));
|
|
73
|
+
geometry.setAttribute("instanceColorMix", new THREE.InstancedBufferAttribute(colorMix, 1));
|
|
74
|
+
geometry.instanceCount = resolved.count;
|
|
75
|
+
geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), Math.max(size.width, size.depth) * 0.75 + resolved.height[1]);
|
|
76
|
+
return geometry;
|
|
77
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
export interface GrassWindOptions {
|
|
3
|
+
direction?: readonly [x: number, z: number];
|
|
4
|
+
strength?: number;
|
|
5
|
+
speed?: number;
|
|
6
|
+
gustScale?: number;
|
|
7
|
+
flutter?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface GrassMaterialOptions {
|
|
10
|
+
colorBase?: THREE.ColorRepresentation;
|
|
11
|
+
colorTip?: THREE.ColorRepresentation;
|
|
12
|
+
colorVariation?: number;
|
|
13
|
+
wind?: GrassWindOptions | false;
|
|
14
|
+
roughness?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface GrassShaderUniforms {
|
|
17
|
+
uTime: THREE.IUniform<number>;
|
|
18
|
+
uWindDirection: THREE.IUniform<THREE.Vector2>;
|
|
19
|
+
uWindStrength: THREE.IUniform<number>;
|
|
20
|
+
uWindSpeed: THREE.IUniform<number>;
|
|
21
|
+
uWindGustScale: THREE.IUniform<number>;
|
|
22
|
+
uWindFlutter: THREE.IUniform<number>;
|
|
23
|
+
uColorBase: THREE.IUniform<THREE.Color>;
|
|
24
|
+
uColorTip: THREE.IUniform<THREE.Color>;
|
|
25
|
+
uColorVariation: THREE.IUniform<number>;
|
|
26
|
+
}
|
|
27
|
+
export interface GrassMaterialHandle {
|
|
28
|
+
material: THREE.MeshStandardMaterial;
|
|
29
|
+
uniforms: GrassShaderUniforms;
|
|
30
|
+
}
|
|
31
|
+
export declare const DEFAULT_GRASS_WIND: Required<GrassWindOptions>;
|
|
32
|
+
export declare function resolveGrassWind(wind: GrassWindOptions | false | undefined): Required<GrassWindOptions>;
|
|
33
|
+
export declare function createGrassMaterial(options?: GrassMaterialOptions): GrassMaterialHandle;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
export const DEFAULT_GRASS_WIND = {
|
|
3
|
+
direction: [1, 0.35],
|
|
4
|
+
strength: 0.22,
|
|
5
|
+
speed: 1.6,
|
|
6
|
+
gustScale: 0.16,
|
|
7
|
+
flutter: 0.08,
|
|
8
|
+
};
|
|
9
|
+
function normalizeWindDirection(direction) {
|
|
10
|
+
const vector = new THREE.Vector2(direction[0], direction[1]);
|
|
11
|
+
return vector.lengthSq() === 0 ? new THREE.Vector2(1, 0) : vector.normalize();
|
|
12
|
+
}
|
|
13
|
+
export function resolveGrassWind(wind) {
|
|
14
|
+
if (wind === false)
|
|
15
|
+
return { ...DEFAULT_GRASS_WIND, strength: 0, flutter: 0 };
|
|
16
|
+
return {
|
|
17
|
+
direction: wind?.direction ?? DEFAULT_GRASS_WIND.direction,
|
|
18
|
+
strength: wind?.strength ?? DEFAULT_GRASS_WIND.strength,
|
|
19
|
+
speed: wind?.speed ?? DEFAULT_GRASS_WIND.speed,
|
|
20
|
+
gustScale: wind?.gustScale ?? DEFAULT_GRASS_WIND.gustScale,
|
|
21
|
+
flutter: wind?.flutter ?? DEFAULT_GRASS_WIND.flutter,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function createGrassMaterial(options = {}) {
|
|
25
|
+
const wind = resolveGrassWind(options.wind);
|
|
26
|
+
const uniforms = {
|
|
27
|
+
uTime: { value: 0 },
|
|
28
|
+
uWindDirection: { value: normalizeWindDirection(wind.direction) },
|
|
29
|
+
uWindStrength: { value: wind.strength },
|
|
30
|
+
uWindSpeed: { value: wind.speed },
|
|
31
|
+
uWindGustScale: { value: wind.gustScale },
|
|
32
|
+
uWindFlutter: { value: wind.flutter },
|
|
33
|
+
uColorBase: { value: new THREE.Color(options.colorBase ?? "#2d431f") },
|
|
34
|
+
uColorTip: { value: new THREE.Color(options.colorTip ?? "#a6cc58") },
|
|
35
|
+
uColorVariation: { value: options.colorVariation ?? 0.28 },
|
|
36
|
+
};
|
|
37
|
+
const material = new THREE.MeshStandardMaterial({
|
|
38
|
+
color: "#ffffff",
|
|
39
|
+
side: THREE.DoubleSide,
|
|
40
|
+
roughness: options.roughness ?? 0.82,
|
|
41
|
+
metalness: 0,
|
|
42
|
+
});
|
|
43
|
+
material.onBeforeCompile = (shader) => {
|
|
44
|
+
Object.assign(shader.uniforms, uniforms);
|
|
45
|
+
shader.vertexShader = shader.vertexShader
|
|
46
|
+
.replace("#include <common>", `
|
|
47
|
+
#include <common>
|
|
48
|
+
attribute vec3 instanceOffset;
|
|
49
|
+
attribute float instanceYaw;
|
|
50
|
+
attribute float instanceHeight;
|
|
51
|
+
attribute float instanceWidth;
|
|
52
|
+
attribute float instanceBend;
|
|
53
|
+
attribute float instancePhase;
|
|
54
|
+
attribute float instanceColorMix;
|
|
55
|
+
uniform float uTime;
|
|
56
|
+
uniform vec2 uWindDirection;
|
|
57
|
+
uniform float uWindStrength;
|
|
58
|
+
uniform float uWindSpeed;
|
|
59
|
+
uniform float uWindGustScale;
|
|
60
|
+
uniform float uWindFlutter;
|
|
61
|
+
varying float vGrassBladeT;
|
|
62
|
+
varying float vGrassColorMix;
|
|
63
|
+
vec3 jgGrassPosition;
|
|
64
|
+
vec3 jgGrassNormal;
|
|
65
|
+
void jgComputeGrassBlade() {
|
|
66
|
+
float t = position.y;
|
|
67
|
+
float side = position.x;
|
|
68
|
+
float taper = (1.0 - t) * (0.72 + 0.28 * (1.0 - t));
|
|
69
|
+
float width = instanceWidth * taper;
|
|
70
|
+
float arc = instanceBend * t * t;
|
|
71
|
+
vec3 localPosition = vec3(side * width, instanceHeight * t - arc * instanceHeight * 0.18, arc * instanceHeight);
|
|
72
|
+
vec3 localNormal = normalize(vec3(0.0, -instanceBend * t, 1.0));
|
|
73
|
+
float cy = cos(instanceYaw);
|
|
74
|
+
float sy = sin(instanceYaw);
|
|
75
|
+
vec3 yawedPosition = vec3(localPosition.x * cy + localPosition.z * sy, localPosition.y, -localPosition.x * sy + localPosition.z * cy);
|
|
76
|
+
vec3 yawedNormal = normalize(vec3(localNormal.x * cy + localNormal.z * sy, localNormal.y, -localNormal.x * sy + localNormal.z * cy));
|
|
77
|
+
float gustPhase = dot(instanceOffset.xz, uWindDirection) * uWindGustScale + uTime * uWindSpeed + instancePhase;
|
|
78
|
+
float gust = sin(gustPhase) * 0.7 + sin(gustPhase * 0.43 + 2.4) * 0.3;
|
|
79
|
+
float flutter = sin(uTime * 7.5 + instancePhase * 2.7) * uWindFlutter;
|
|
80
|
+
vec2 windOffset = uWindDirection * (gust * uWindStrength + flutter) * t * t;
|
|
81
|
+
jgGrassPosition = instanceOffset + yawedPosition + vec3(windOffset.x, 0.0, windOffset.y);
|
|
82
|
+
jgGrassNormal = yawedNormal;
|
|
83
|
+
vGrassBladeT = t;
|
|
84
|
+
vGrassColorMix = instanceColorMix;
|
|
85
|
+
}
|
|
86
|
+
`)
|
|
87
|
+
.replace("#include <beginnormal_vertex>", "jgComputeGrassBlade();\nvec3 objectNormal = jgGrassNormal;")
|
|
88
|
+
.replace("#include <begin_vertex>", "vec3 transformed = jgGrassPosition;");
|
|
89
|
+
shader.fragmentShader = shader.fragmentShader
|
|
90
|
+
.replace("#include <common>", `
|
|
91
|
+
#include <common>
|
|
92
|
+
uniform vec3 uColorBase;
|
|
93
|
+
uniform vec3 uColorTip;
|
|
94
|
+
uniform float uColorVariation;
|
|
95
|
+
varying float vGrassBladeT;
|
|
96
|
+
varying float vGrassColorMix;
|
|
97
|
+
`)
|
|
98
|
+
.replace("#include <color_fragment>", `
|
|
99
|
+
#include <color_fragment>
|
|
100
|
+
vec3 grassColor = mix(uColorBase, uColorTip, smoothstep(0.0, 1.0, vGrassBladeT));
|
|
101
|
+
grassColor *= mix(1.0 - uColorVariation, 1.0 + uColorVariation, vGrassColorMix);
|
|
102
|
+
grassColor *= mix(0.58, 1.0, smoothstep(0.05, 0.45, vGrassBladeT));
|
|
103
|
+
diffuseColor.rgb = grassColor;
|
|
104
|
+
`);
|
|
105
|
+
};
|
|
106
|
+
material.customProgramCacheKey = () => `jgengine-grass-${wind.strength}-${wind.flutter}`;
|
|
107
|
+
return { material, uniforms };
|
|
108
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { GrassField, type GrassFieldProps } from "./GrassField.js";
|
|
2
|
+
export { ProceduralGround, type ProceduralGroundProps } from "./ProceduralGround.js";
|
|
3
|
+
export { createGrassBladeGeometry, resolveGrassBladeGeometryOptions, resolveGrassRange, type GrassBladeGeometryOptions, type GrassRange, type ResolvedGrassBladeGeometryOptions, } from "./grassGeometry.js";
|
|
4
|
+
export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, type GrassMaterialHandle, type GrassMaterialOptions, type GrassShaderUniforms, type GrassWindOptions, } from "./grassMaterial.js";
|
|
5
|
+
export { createSeededRandom, hashNoise2, seedToUint32, type TerrainSeed } from "./random.js";
|
|
6
|
+
export { createProceduralGroundGeometry, createProceduralTerrainSampler, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, type ProceduralTerrainConfig, type ResolvedTerrainSegments, type ResolvedTerrainSize, type TerrainArea, type TerrainHeightSampler, type TerrainVertexColorOptions, } from "./terrainMath.js";
|
|
7
|
+
export { arenaField, flatField, fractalNoise, noiseField, resolveGroundStep, resolveTerrainField, valueNoise, withNormal, type FractalNoiseConfig, type NoiseFieldConfig, type TerrainField, type TerrainNormal, } from "@jgengine/core/world/terrain";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { GrassField } from "./GrassField.js";
|
|
2
|
+
export { ProceduralGround } from "./ProceduralGround.js";
|
|
3
|
+
export { createGrassBladeGeometry, resolveGrassBladeGeometryOptions, resolveGrassRange, } from "./grassGeometry.js";
|
|
4
|
+
export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, } from "./grassMaterial.js";
|
|
5
|
+
export { createSeededRandom, hashNoise2, seedToUint32 } from "./random.js";
|
|
6
|
+
export { createProceduralGroundGeometry, createProceduralTerrainSampler, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, } from "./terrainMath.js";
|
|
7
|
+
export { arenaField, flatField, fractalNoise, noiseField, resolveGroundStep, resolveTerrainField, valueNoise, withNormal, } from "@jgengine/core/world/terrain";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type TerrainSeed = number | string;
|
|
2
|
+
export declare function seedToUint32(seed?: TerrainSeed): number;
|
|
3
|
+
export declare function createSeededRandom(seed?: TerrainSeed): () => number;
|
|
4
|
+
export declare function hashNoise2(x: number, z: number, seed?: TerrainSeed): number;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function seedToUint32(seed = 1) {
|
|
2
|
+
if (typeof seed === "number")
|
|
3
|
+
return seed >>> 0;
|
|
4
|
+
let hash = 2166136261;
|
|
5
|
+
for (let index = 0; index < seed.length; index += 1) {
|
|
6
|
+
hash ^= seed.charCodeAt(index);
|
|
7
|
+
hash = Math.imul(hash, 16777619);
|
|
8
|
+
}
|
|
9
|
+
return hash >>> 0;
|
|
10
|
+
}
|
|
11
|
+
export function createSeededRandom(seed = 1) {
|
|
12
|
+
let state = seedToUint32(seed);
|
|
13
|
+
return () => {
|
|
14
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
15
|
+
let value = state;
|
|
16
|
+
value = Math.imul(value ^ (value >>> 15), value | 1);
|
|
17
|
+
value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
|
|
18
|
+
return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function hashNoise2(x, z, seed = 1) {
|
|
22
|
+
let hash = seedToUint32(seed);
|
|
23
|
+
hash ^= Math.imul(x | 0, 374761393);
|
|
24
|
+
hash ^= Math.imul(z | 0, 668265263);
|
|
25
|
+
hash = Math.imul(hash ^ (hash >>> 13), 1274126177);
|
|
26
|
+
return ((hash ^ (hash >>> 16)) >>> 0) / 4294967295;
|
|
27
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type NoiseFieldConfig } from "@jgengine/core/world/terrain";
|
|
2
|
+
import * as THREE from "three";
|
|
3
|
+
import { type TerrainSeed } from "./random.js";
|
|
4
|
+
export type TerrainArea = number | readonly [width: number, depth: number];
|
|
5
|
+
export type TerrainHeightSampler = (x: number, z: number) => number;
|
|
6
|
+
export interface ProceduralTerrainConfig {
|
|
7
|
+
size?: TerrainArea;
|
|
8
|
+
segments?: number | readonly [x: number, z: number];
|
|
9
|
+
seed?: TerrainSeed;
|
|
10
|
+
height?: number;
|
|
11
|
+
moundScale?: number;
|
|
12
|
+
octaves?: number;
|
|
13
|
+
ridged?: boolean;
|
|
14
|
+
baseOffset?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface ResolvedTerrainSize {
|
|
17
|
+
width: number;
|
|
18
|
+
depth: number;
|
|
19
|
+
}
|
|
20
|
+
export interface ResolvedTerrainSegments {
|
|
21
|
+
x: number;
|
|
22
|
+
z: number;
|
|
23
|
+
}
|
|
24
|
+
export interface TerrainVertexColorOptions {
|
|
25
|
+
low?: THREE.ColorRepresentation;
|
|
26
|
+
high?: THREE.ColorRepresentation;
|
|
27
|
+
waterline?: THREE.ColorRepresentation;
|
|
28
|
+
waterlineHeight?: number;
|
|
29
|
+
}
|
|
30
|
+
export declare function resolveTerrainSize(size?: TerrainArea): ResolvedTerrainSize;
|
|
31
|
+
export declare function resolveTerrainSegments(segments?: ProceduralTerrainConfig["segments"]): ResolvedTerrainSegments;
|
|
32
|
+
export declare function toNoiseFieldConfig(config?: ProceduralTerrainConfig): NoiseFieldConfig;
|
|
33
|
+
export declare function createProceduralTerrainSampler(config?: ProceduralTerrainConfig): TerrainHeightSampler;
|
|
34
|
+
export declare function createProceduralGroundGeometry(config?: ProceduralTerrainConfig, colors?: TerrainVertexColorOptions): THREE.BufferGeometry;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { noiseField } from "@jgengine/core/world/terrain";
|
|
2
|
+
import * as THREE from "three";
|
|
3
|
+
import {} from "./random.js";
|
|
4
|
+
export function resolveTerrainSize(size = 40) {
|
|
5
|
+
return typeof size === "number" ? { width: size, depth: size } : { width: size[0], depth: size[1] };
|
|
6
|
+
}
|
|
7
|
+
export function resolveTerrainSegments(segments = 96) {
|
|
8
|
+
if (typeof segments !== "number")
|
|
9
|
+
return { x: Math.max(1, Math.floor(segments[0])), z: Math.max(1, Math.floor(segments[1])) };
|
|
10
|
+
const resolved = Math.max(1, Math.floor(segments));
|
|
11
|
+
return { x: resolved, z: resolved };
|
|
12
|
+
}
|
|
13
|
+
export function toNoiseFieldConfig(config = {}) {
|
|
14
|
+
return {
|
|
15
|
+
seed: config.seed,
|
|
16
|
+
amplitude: config.height ?? 1.4,
|
|
17
|
+
frequency: config.moundScale ?? 0.075,
|
|
18
|
+
octaves: config.octaves ?? 4,
|
|
19
|
+
ridged: config.ridged ?? false,
|
|
20
|
+
baseHeight: config.baseOffset ?? 0,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function createProceduralTerrainSampler(config = {}) {
|
|
24
|
+
return noiseField(toNoiseFieldConfig(config)).sampleHeight;
|
|
25
|
+
}
|
|
26
|
+
export function createProceduralGroundGeometry(config = {}, colors = {}) {
|
|
27
|
+
const size = resolveTerrainSize(config.size);
|
|
28
|
+
const segments = resolveTerrainSegments(config.segments);
|
|
29
|
+
const sampler = createProceduralTerrainSampler(config);
|
|
30
|
+
const vertexCountX = segments.x + 1;
|
|
31
|
+
const vertexCountZ = segments.z + 1;
|
|
32
|
+
const positions = new Float32Array(vertexCountX * vertexCountZ * 3);
|
|
33
|
+
const uvs = new Float32Array(vertexCountX * vertexCountZ * 2);
|
|
34
|
+
const colorValues = new Float32Array(vertexCountX * vertexCountZ * 3);
|
|
35
|
+
const indices = new Uint32Array(segments.x * segments.z * 6);
|
|
36
|
+
const low = new THREE.Color(colors.low ?? "#30402c");
|
|
37
|
+
const high = new THREE.Color(colors.high ?? "#7f8b50");
|
|
38
|
+
const waterline = colors.waterline === undefined ? null : new THREE.Color(colors.waterline);
|
|
39
|
+
const minHeight = (config.baseOffset ?? 0) - (config.height ?? 1.4) * 1.2;
|
|
40
|
+
const maxHeight = (config.baseOffset ?? 0) + (config.height ?? 1.4) * 1.2;
|
|
41
|
+
let index = 0;
|
|
42
|
+
let uvIndex = 0;
|
|
43
|
+
let colorIndex = 0;
|
|
44
|
+
for (let zIndex = 0; zIndex < vertexCountZ; zIndex += 1) {
|
|
45
|
+
const v = zIndex / segments.z;
|
|
46
|
+
const z = (v - 0.5) * size.depth;
|
|
47
|
+
for (let xIndex = 0; xIndex < vertexCountX; xIndex += 1) {
|
|
48
|
+
const u = xIndex / segments.x;
|
|
49
|
+
const x = (u - 0.5) * size.width;
|
|
50
|
+
const y = sampler(x, z);
|
|
51
|
+
positions[index] = x;
|
|
52
|
+
positions[index + 1] = y;
|
|
53
|
+
positions[index + 2] = z;
|
|
54
|
+
uvs[uvIndex] = u;
|
|
55
|
+
uvs[uvIndex + 1] = v;
|
|
56
|
+
const blend = THREE.MathUtils.clamp((y - minHeight) / (maxHeight - minHeight), 0, 1);
|
|
57
|
+
const color = low.clone().lerp(high, blend);
|
|
58
|
+
if (waterline !== null && y <= (colors.waterlineHeight ?? 0))
|
|
59
|
+
color.lerp(waterline, 0.65);
|
|
60
|
+
colorValues[colorIndex] = color.r;
|
|
61
|
+
colorValues[colorIndex + 1] = color.g;
|
|
62
|
+
colorValues[colorIndex + 2] = color.b;
|
|
63
|
+
index += 3;
|
|
64
|
+
uvIndex += 2;
|
|
65
|
+
colorIndex += 3;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
let triangleIndex = 0;
|
|
69
|
+
for (let zIndex = 0; zIndex < segments.z; zIndex += 1) {
|
|
70
|
+
for (let xIndex = 0; xIndex < segments.x; xIndex += 1) {
|
|
71
|
+
const a = zIndex * vertexCountX + xIndex;
|
|
72
|
+
const b = a + 1;
|
|
73
|
+
const c = a + vertexCountX;
|
|
74
|
+
const d = c + 1;
|
|
75
|
+
indices[triangleIndex] = a;
|
|
76
|
+
indices[triangleIndex + 1] = c;
|
|
77
|
+
indices[triangleIndex + 2] = b;
|
|
78
|
+
indices[triangleIndex + 3] = b;
|
|
79
|
+
indices[triangleIndex + 4] = c;
|
|
80
|
+
indices[triangleIndex + 5] = d;
|
|
81
|
+
triangleIndex += 6;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const geometry = new THREE.BufferGeometry();
|
|
85
|
+
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
|
86
|
+
geometry.setAttribute("uv", new THREE.BufferAttribute(uvs, 2));
|
|
87
|
+
geometry.setAttribute("color", new THREE.BufferAttribute(colorValues, 3));
|
|
88
|
+
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
|
|
89
|
+
geometry.computeVertexNormals();
|
|
90
|
+
geometry.computeBoundingSphere();
|
|
91
|
+
return geometry;
|
|
92
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./terrain/index.js";
|
package/dist/terrain.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./terrain/index.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ThreeElements } from "@react-three/fiber";
|
|
2
|
+
import { type OceanConfig } from "./OceanConfig.js";
|
|
3
|
+
export interface OceanProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material"> {
|
|
4
|
+
config?: OceanConfig;
|
|
5
|
+
}
|
|
6
|
+
export declare function Ocean({ config, ...meshProps }: OceanProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useFrame } from "@react-three/fiber";
|
|
3
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { createOceanConfig } from "./OceanConfig.js";
|
|
6
|
+
import { createOceanMaterial, syncOceanMaterial } from "./OceanMaterial.js";
|
|
7
|
+
function createOceanGeometry(config) {
|
|
8
|
+
const geometry = new THREE.PlaneGeometry(config.size, config.size, config.resolution, config.resolution);
|
|
9
|
+
geometry.rotateX(-Math.PI / 2);
|
|
10
|
+
geometry.computeBoundingSphere();
|
|
11
|
+
return geometry;
|
|
12
|
+
}
|
|
13
|
+
export function Ocean({ config, ...meshProps }) {
|
|
14
|
+
const resolved = useMemo(() => createOceanConfig(config), [config]);
|
|
15
|
+
const elapsedRef = useRef(0);
|
|
16
|
+
const geometry = useMemo(() => createOceanGeometry(resolved), [resolved]);
|
|
17
|
+
const material = useMemo(() => createOceanMaterial(resolved), [resolved]);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
return () => geometry.dispose();
|
|
20
|
+
}, [geometry]);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
elapsedRef.current = 0;
|
|
23
|
+
return () => material.dispose();
|
|
24
|
+
}, [material]);
|
|
25
|
+
useFrame((_, delta) => {
|
|
26
|
+
elapsedRef.current += delta * resolved.timeScale;
|
|
27
|
+
syncOceanMaterial(material, resolved, elapsedRef.current);
|
|
28
|
+
});
|
|
29
|
+
return _jsx("mesh", { ...meshProps, geometry: geometry, material: material });
|
|
30
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
export declare const MAX_OCEAN_WAVES = 6;
|
|
3
|
+
export type OceanQualityPreset = "low" | "medium" | "high" | "ultra";
|
|
4
|
+
export interface OceanDirectionVector {
|
|
5
|
+
x: number;
|
|
6
|
+
z: number;
|
|
7
|
+
}
|
|
8
|
+
export type OceanWaveDirection = number | OceanDirectionVector;
|
|
9
|
+
export interface OceanWaveConfig {
|
|
10
|
+
amplitude?: number;
|
|
11
|
+
wavelength?: number;
|
|
12
|
+
speed?: number;
|
|
13
|
+
direction?: OceanWaveDirection;
|
|
14
|
+
steepness?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface OceanColorConfig {
|
|
17
|
+
shallow?: THREE.ColorRepresentation;
|
|
18
|
+
deep?: THREE.ColorRepresentation;
|
|
19
|
+
crest?: THREE.ColorRepresentation;
|
|
20
|
+
foam?: THREE.ColorRepresentation;
|
|
21
|
+
opacity?: number;
|
|
22
|
+
fresnelStrength?: number;
|
|
23
|
+
horizonBlend?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface OceanFoamConfig {
|
|
26
|
+
crestThreshold?: number;
|
|
27
|
+
softness?: number;
|
|
28
|
+
intensity?: number;
|
|
29
|
+
coverage?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface OceanConfig {
|
|
32
|
+
quality?: OceanQualityPreset;
|
|
33
|
+
size?: number;
|
|
34
|
+
resolution?: number;
|
|
35
|
+
amplitude?: number;
|
|
36
|
+
speed?: number;
|
|
37
|
+
direction?: OceanWaveDirection;
|
|
38
|
+
choppiness?: number;
|
|
39
|
+
steepness?: number;
|
|
40
|
+
timeScale?: number;
|
|
41
|
+
color?: OceanColorConfig;
|
|
42
|
+
foam?: OceanFoamConfig;
|
|
43
|
+
waves?: readonly OceanWaveConfig[];
|
|
44
|
+
}
|
|
45
|
+
export interface ResolvedOceanColorConfig {
|
|
46
|
+
shallow: THREE.ColorRepresentation;
|
|
47
|
+
deep: THREE.ColorRepresentation;
|
|
48
|
+
crest: THREE.ColorRepresentation;
|
|
49
|
+
foam: THREE.ColorRepresentation;
|
|
50
|
+
opacity: number;
|
|
51
|
+
fresnelStrength: number;
|
|
52
|
+
horizonBlend: number;
|
|
53
|
+
}
|
|
54
|
+
export interface ResolvedOceanFoamConfig {
|
|
55
|
+
crestThreshold: number;
|
|
56
|
+
softness: number;
|
|
57
|
+
intensity: number;
|
|
58
|
+
coverage: number;
|
|
59
|
+
}
|
|
60
|
+
export interface ResolvedOceanWaveConfig {
|
|
61
|
+
amplitude: number;
|
|
62
|
+
wavelength: number;
|
|
63
|
+
speed: number;
|
|
64
|
+
direction: OceanDirectionVector;
|
|
65
|
+
steepness: number;
|
|
66
|
+
}
|
|
67
|
+
export interface ResolvedOceanConfig {
|
|
68
|
+
quality: OceanQualityPreset;
|
|
69
|
+
size: number;
|
|
70
|
+
resolution: number;
|
|
71
|
+
amplitude: number;
|
|
72
|
+
speed: number;
|
|
73
|
+
direction: OceanDirectionVector;
|
|
74
|
+
choppiness: number;
|
|
75
|
+
steepness: number;
|
|
76
|
+
timeScale: number;
|
|
77
|
+
color: ResolvedOceanColorConfig;
|
|
78
|
+
foam: ResolvedOceanFoamConfig;
|
|
79
|
+
waves: readonly ResolvedOceanWaveConfig[];
|
|
80
|
+
}
|
|
81
|
+
export declare const OCEAN_QUALITY_PRESETS: Record<OceanQualityPreset, {
|
|
82
|
+
size: number;
|
|
83
|
+
resolution: number;
|
|
84
|
+
}>;
|
|
85
|
+
export declare const DEFAULT_OCEAN_CONFIG: ResolvedOceanConfig;
|
|
86
|
+
export declare function createOceanConfig(patch?: OceanConfig): ResolvedOceanConfig;
|
|
87
|
+
export declare function buildOceanWaveUniforms(config: ResolvedOceanConfig): {
|
|
88
|
+
directions: THREE.Vector2[];
|
|
89
|
+
params: THREE.Vector4[];
|
|
90
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
export const MAX_OCEAN_WAVES = 6;
|
|
3
|
+
export const OCEAN_QUALITY_PRESETS = {
|
|
4
|
+
low: { size: 220, resolution: 80 },
|
|
5
|
+
medium: { size: 360, resolution: 128 },
|
|
6
|
+
high: { size: 520, resolution: 192 },
|
|
7
|
+
ultra: { size: 720, resolution: 256 },
|
|
8
|
+
};
|
|
9
|
+
export const DEFAULT_OCEAN_CONFIG = {
|
|
10
|
+
quality: "medium",
|
|
11
|
+
size: OCEAN_QUALITY_PRESETS.medium.size,
|
|
12
|
+
resolution: OCEAN_QUALITY_PRESETS.medium.resolution,
|
|
13
|
+
amplitude: 1,
|
|
14
|
+
speed: 1,
|
|
15
|
+
direction: { x: 0.78, z: 0.62 },
|
|
16
|
+
choppiness: 1,
|
|
17
|
+
steepness: 0.72,
|
|
18
|
+
timeScale: 1,
|
|
19
|
+
color: {
|
|
20
|
+
shallow: "#2dc5d3",
|
|
21
|
+
deep: "#064468",
|
|
22
|
+
crest: "#8be9ff",
|
|
23
|
+
foam: "#f4fbff",
|
|
24
|
+
opacity: 0.88,
|
|
25
|
+
fresnelStrength: 0.7,
|
|
26
|
+
horizonBlend: 0.38,
|
|
27
|
+
},
|
|
28
|
+
foam: {
|
|
29
|
+
crestThreshold: 0.64,
|
|
30
|
+
softness: 0.22,
|
|
31
|
+
intensity: 0.8,
|
|
32
|
+
coverage: 0.45,
|
|
33
|
+
},
|
|
34
|
+
waves: [],
|
|
35
|
+
};
|
|
36
|
+
const DEFAULT_WAVE_SHAPE = [
|
|
37
|
+
{ amplitude: 1.15, wavelength: 44, speed: 1, steepness: 0.68 },
|
|
38
|
+
{ amplitude: 0.72, wavelength: 28, speed: 1.08, steepness: 0.58 },
|
|
39
|
+
{ amplitude: 0.44, wavelength: 17, speed: 1.18, steepness: 0.48 },
|
|
40
|
+
{ amplitude: 0.28, wavelength: 10, speed: 1.34, steepness: 0.38 },
|
|
41
|
+
{ amplitude: 0.18, wavelength: 6.5, speed: 1.52, steepness: 0.28 },
|
|
42
|
+
{ amplitude: 0.11, wavelength: 3.8, speed: 1.75, steepness: 0.18 },
|
|
43
|
+
];
|
|
44
|
+
const DEFAULT_DIRECTION_OFFSETS = [-8, 7, 21, -31, 46, -58];
|
|
45
|
+
function finiteOr(value, fallback) {
|
|
46
|
+
return value === undefined || !Number.isFinite(value) ? fallback : value;
|
|
47
|
+
}
|
|
48
|
+
function clamp(value, min, max) {
|
|
49
|
+
return Math.min(max, Math.max(min, value));
|
|
50
|
+
}
|
|
51
|
+
function directionFromAngle(degrees) {
|
|
52
|
+
const radians = THREE.MathUtils.degToRad(degrees);
|
|
53
|
+
return { x: Math.cos(radians), z: Math.sin(radians) };
|
|
54
|
+
}
|
|
55
|
+
function directionAngle(direction) {
|
|
56
|
+
return THREE.MathUtils.radToDeg(Math.atan2(direction.z, direction.x));
|
|
57
|
+
}
|
|
58
|
+
function normalizeDirection(direction, fallback) {
|
|
59
|
+
if (direction === undefined)
|
|
60
|
+
return fallback;
|
|
61
|
+
if (typeof direction === "number")
|
|
62
|
+
return directionFromAngle(direction);
|
|
63
|
+
const length = Math.hypot(direction.x, direction.z);
|
|
64
|
+
if (length <= 0.0001 || !Number.isFinite(length))
|
|
65
|
+
return fallback;
|
|
66
|
+
return { x: direction.x / length, z: direction.z / length };
|
|
67
|
+
}
|
|
68
|
+
function createDefaultWaves(config) {
|
|
69
|
+
const baseAngle = directionAngle(config.direction);
|
|
70
|
+
return DEFAULT_WAVE_SHAPE.map((wave, index) => ({
|
|
71
|
+
amplitude: wave.amplitude,
|
|
72
|
+
wavelength: wave.wavelength,
|
|
73
|
+
speed: wave.speed,
|
|
74
|
+
direction: directionFromAngle(baseAngle + DEFAULT_DIRECTION_OFFSETS[index]),
|
|
75
|
+
steepness: wave.steepness,
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
function resolveWave(wave, fallback, config) {
|
|
79
|
+
return {
|
|
80
|
+
amplitude: Math.max(0, finiteOr(wave?.amplitude, fallback.amplitude) * config.amplitude),
|
|
81
|
+
wavelength: Math.max(0.1, finiteOr(wave?.wavelength, fallback.wavelength)),
|
|
82
|
+
speed: finiteOr(wave?.speed, fallback.speed) * config.speed,
|
|
83
|
+
direction: normalizeDirection(wave?.direction, fallback.direction),
|
|
84
|
+
steepness: clamp(finiteOr(wave?.steepness, fallback.steepness) * config.steepness, 0, 1.2),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export function createOceanConfig(patch = {}) {
|
|
88
|
+
const quality = patch.quality ?? DEFAULT_OCEAN_CONFIG.quality;
|
|
89
|
+
const preset = OCEAN_QUALITY_PRESETS[quality];
|
|
90
|
+
const withoutWaves = {
|
|
91
|
+
quality,
|
|
92
|
+
size: Math.max(1, Math.floor(finiteOr(patch.size, preset.size))),
|
|
93
|
+
resolution: Math.max(1, Math.floor(finiteOr(patch.resolution, preset.resolution))),
|
|
94
|
+
amplitude: Math.max(0, finiteOr(patch.amplitude, DEFAULT_OCEAN_CONFIG.amplitude)),
|
|
95
|
+
speed: finiteOr(patch.speed, DEFAULT_OCEAN_CONFIG.speed),
|
|
96
|
+
direction: normalizeDirection(patch.direction, DEFAULT_OCEAN_CONFIG.direction),
|
|
97
|
+
choppiness: Math.max(0, finiteOr(patch.choppiness, DEFAULT_OCEAN_CONFIG.choppiness)),
|
|
98
|
+
steepness: Math.max(0, finiteOr(patch.steepness, DEFAULT_OCEAN_CONFIG.steepness)),
|
|
99
|
+
timeScale: finiteOr(patch.timeScale, DEFAULT_OCEAN_CONFIG.timeScale),
|
|
100
|
+
color: {
|
|
101
|
+
...DEFAULT_OCEAN_CONFIG.color,
|
|
102
|
+
...patch.color,
|
|
103
|
+
opacity: clamp(finiteOr(patch.color?.opacity, DEFAULT_OCEAN_CONFIG.color.opacity), 0, 1),
|
|
104
|
+
fresnelStrength: Math.max(0, finiteOr(patch.color?.fresnelStrength, DEFAULT_OCEAN_CONFIG.color.fresnelStrength)),
|
|
105
|
+
horizonBlend: clamp(finiteOr(patch.color?.horizonBlend, DEFAULT_OCEAN_CONFIG.color.horizonBlend), 0, 1),
|
|
106
|
+
},
|
|
107
|
+
foam: {
|
|
108
|
+
...DEFAULT_OCEAN_CONFIG.foam,
|
|
109
|
+
...patch.foam,
|
|
110
|
+
crestThreshold: clamp(finiteOr(patch.foam?.crestThreshold, DEFAULT_OCEAN_CONFIG.foam.crestThreshold), 0, 1),
|
|
111
|
+
softness: clamp(finiteOr(patch.foam?.softness, DEFAULT_OCEAN_CONFIG.foam.softness), 0.001, 1),
|
|
112
|
+
intensity: Math.max(0, finiteOr(patch.foam?.intensity, DEFAULT_OCEAN_CONFIG.foam.intensity)),
|
|
113
|
+
coverage: clamp(finiteOr(patch.foam?.coverage, DEFAULT_OCEAN_CONFIG.foam.coverage), 0, 1),
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
const defaultWaves = createDefaultWaves(withoutWaves);
|
|
117
|
+
const inputWaves = patch.waves ?? defaultWaves;
|
|
118
|
+
const waves = Array.from({ length: MAX_OCEAN_WAVES }, (_, index) => resolveWave(inputWaves[index], defaultWaves[index], withoutWaves));
|
|
119
|
+
return { ...withoutWaves, waves };
|
|
120
|
+
}
|
|
121
|
+
export function buildOceanWaveUniforms(config) {
|
|
122
|
+
const directions = [];
|
|
123
|
+
const params = [];
|
|
124
|
+
for (const wave of config.waves) {
|
|
125
|
+
const waveNumber = (Math.PI * 2) / wave.wavelength;
|
|
126
|
+
const omega = Math.sqrt(9.81 * waveNumber) * wave.speed;
|
|
127
|
+
directions.push(new THREE.Vector2(wave.direction.x, wave.direction.z));
|
|
128
|
+
params.push(new THREE.Vector4(waveNumber, wave.amplitude, wave.steepness, omega));
|
|
129
|
+
}
|
|
130
|
+
return { directions, params };
|
|
131
|
+
}
|