@jgengine/shell 0.6.0 → 0.8.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 +214 -2
- package/README.md +26 -1
- package/dist/GameHost.d.ts +12 -0
- package/dist/GameHost.js +33 -0
- package/dist/GamePlayer.d.ts +11 -0
- package/dist/GamePlayer.js +24 -0
- package/dist/GamePlayerShell.d.ts +33 -1
- package/dist/GamePlayerShell.js +641 -59
- package/dist/audio/AudioComponents.d.ts +12 -0
- package/dist/audio/AudioComponents.js +53 -0
- package/dist/audio/audioEngine.d.ts +23 -0
- package/dist/audio/audioEngine.js +110 -0
- package/dist/behaviour.d.ts +31 -0
- package/dist/behaviour.js +63 -0
- package/dist/camera/GameCameraRig.d.ts +16 -0
- package/dist/camera/GameCameraRig.js +56 -0
- package/dist/camera/GameFirstPersonCamera.js +2 -0
- package/dist/camera/GameInspectionCamera.d.ts +6 -0
- package/dist/camera/GameInspectionCamera.js +24 -0
- package/dist/camera/GameOrbitCamera.d.ts +3 -1
- package/dist/camera/GameOrbitCamera.js +4 -2
- package/dist/camera/cameraRigs.d.ts +26 -0
- package/dist/camera/cameraRigs.js +522 -0
- package/dist/camera/index.d.ts +7 -0
- package/dist/camera/index.js +7 -0
- package/dist/camera/inspectionCameraMath.d.ts +25 -0
- package/dist/camera/inspectionCameraMath.js +44 -0
- package/dist/camera/rigMath.d.ts +169 -0
- package/dist/camera/rigMath.js +393 -0
- package/dist/camera/rigResolve.d.ts +15 -0
- package/dist/camera/rigResolve.js +52 -0
- package/dist/camera/shakeChannel.d.ts +29 -0
- package/dist/camera/shakeChannel.js +38 -0
- package/dist/defineGame.d.ts +16 -0
- package/dist/defineGame.js +51 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +14 -0
- package/dist/devtools/DevtoolsOverlay.js +334 -0
- package/dist/environment/Daylight.d.ts +50 -0
- package/dist/environment/Daylight.js +100 -0
- package/dist/environment/EnvironmentScene.js +18 -26
- package/dist/environment/GroundPad.d.ts +7 -0
- package/dist/environment/GroundPad.js +12 -0
- package/dist/environment/daylightCycle.d.ts +26 -0
- package/dist/environment/daylightCycle.js +116 -0
- package/dist/environment/groundPadMath.d.ts +13 -0
- package/dist/environment/groundPadMath.js +10 -0
- package/dist/environment/index.d.ts +2 -0
- package/dist/environment/index.js +2 -0
- package/dist/map/MapMarkerBeacons.d.ts +12 -0
- package/dist/map/MapMarkerBeacons.js +16 -0
- package/dist/map/index.d.ts +2 -0
- package/dist/map/index.js +2 -0
- package/dist/map/terrainMap.d.ts +23 -0
- package/dist/map/terrainMap.js +79 -0
- package/dist/materialOverride.d.ts +8 -0
- package/dist/materialOverride.js +32 -0
- package/dist/multiplayer.d.ts +16 -9
- package/dist/multiplayer.js +62 -12
- package/dist/pointer/PointerOverlays.d.ts +12 -0
- package/dist/pointer/PointerOverlays.js +17 -0
- package/dist/pointer/PointerProbe.d.ts +4 -0
- package/dist/pointer/PointerProbe.js +40 -0
- package/dist/pointer/pointerService.d.ts +26 -0
- package/dist/pointer/pointerService.js +89 -0
- package/dist/registry.d.ts +4 -1
- package/dist/registry.js +3 -1
- package/dist/render/modelRender.d.ts +14 -0
- package/dist/render/modelRender.js +79 -0
- package/dist/replay/useSessionRecorder.d.ts +14 -0
- package/dist/replay/useSessionRecorder.js +22 -0
- package/dist/structures/PlacementGhost.d.ts +8 -0
- package/dist/structures/PlacementGhost.js +11 -0
- package/dist/structures/index.d.ts +2 -0
- package/dist/structures/index.js +2 -0
- package/dist/structures.d.ts +1 -0
- package/dist/structures.js +1 -0
- package/dist/terrain/CarvedTerrain.d.ts +22 -0
- package/dist/terrain/CarvedTerrain.js +19 -0
- package/dist/terrain/EditableGround.d.ts +11 -0
- package/dist/terrain/EditableGround.js +38 -0
- package/dist/terrain/TerraformBrushCursor.d.ts +8 -0
- package/dist/terrain/TerraformBrushCursor.js +13 -0
- package/dist/terrain/index.d.ts +4 -1
- package/dist/terrain/index.js +4 -1
- package/dist/terrain/terrainMath.d.ts +15 -1
- package/dist/terrain/terrainMath.js +87 -1
- package/dist/touch/TouchControlsOverlay.d.ts +18 -0
- package/dist/touch/TouchControlsOverlay.js +173 -0
- package/dist/vision/FrustumSensorHud.d.ts +21 -0
- package/dist/vision/FrustumSensorHud.js +67 -0
- package/dist/vision/HiddenStateProbeHud.d.ts +11 -0
- package/dist/vision/HiddenStateProbeHud.js +11 -0
- package/dist/vision/RevealVision.d.ts +28 -0
- package/dist/vision/RevealVision.js +53 -0
- package/dist/water/OceanShader.d.ts +1 -1
- package/dist/water/OceanShader.js +3 -3
- package/dist/weather/FireSpreadLayer.d.ts +14 -0
- package/dist/weather/FireSpreadLayer.js +68 -0
- package/dist/weather/index.d.ts +1 -0
- package/dist/weather/index.js +1 -0
- package/dist/world/DataObjects.d.ts +44 -0
- package/dist/world/DataObjects.js +75 -0
- package/dist/world/GridWorldScene.d.ts +10 -0
- package/dist/world/GridWorldScene.js +54 -0
- package/dist/world/InstancedJoints.d.ts +14 -0
- package/dist/world/InstancedJoints.js +32 -0
- package/dist/world/WorldHud.d.ts +7 -1
- package/dist/world/WorldHud.js +76 -14
- package/dist/world/WorldItems.d.ts +5 -0
- package/dist/world/WorldItems.js +50 -0
- package/dist/world/floatTextStyle.d.ts +14 -0
- package/dist/world/floatTextStyle.js +39 -0
- package/llms.txt +2382 -0
- package/package.json +10 -5
- package/dist/demo/demoGame.d.ts +0 -2
- package/dist/demo/demoGame.js +0 -208
- package/dist/demo/environmentShowcase.d.ts +0 -2
- package/dist/demo/environmentShowcase.js +0 -15
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useThree } from "@react-three/fiber";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
export function PointerProbe({ service }) {
|
|
4
|
+
const camera = useThree((state) => state.camera);
|
|
5
|
+
const scene = useThree((state) => state.scene);
|
|
6
|
+
const gl = useThree((state) => state.gl);
|
|
7
|
+
const size = useThree((state) => state.size);
|
|
8
|
+
service.bind({ camera, scene, width: size.width, height: size.height });
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const el = gl.domElement;
|
|
11
|
+
let locked = false;
|
|
12
|
+
const onMove = (event) => {
|
|
13
|
+
if (locked)
|
|
14
|
+
return;
|
|
15
|
+
const rect = el.getBoundingClientRect();
|
|
16
|
+
const x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
|
17
|
+
const y = -(((event.clientY - rect.top) / rect.height) * 2 - 1);
|
|
18
|
+
service.setCursor(x, y, true);
|
|
19
|
+
};
|
|
20
|
+
const onLeave = () => {
|
|
21
|
+
if (!locked)
|
|
22
|
+
service.setCursor(0, 0, false);
|
|
23
|
+
};
|
|
24
|
+
const onLockChange = () => {
|
|
25
|
+
locked = document.pointerLockElement === el;
|
|
26
|
+
if (locked)
|
|
27
|
+
service.setCursor(0, 0, true);
|
|
28
|
+
};
|
|
29
|
+
el.addEventListener("pointermove", onMove);
|
|
30
|
+
el.addEventListener("pointerleave", onLeave);
|
|
31
|
+
document.addEventListener("pointerlockchange", onLockChange);
|
|
32
|
+
return () => {
|
|
33
|
+
el.removeEventListener("pointermove", onMove);
|
|
34
|
+
el.removeEventListener("pointerleave", onLeave);
|
|
35
|
+
document.removeEventListener("pointerlockchange", onLockChange);
|
|
36
|
+
service.bind(null);
|
|
37
|
+
};
|
|
38
|
+
}, [gl, service]);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import type { PointerHit, PointerVec3 } from "@jgengine/core/input/pointer";
|
|
3
|
+
export declare const POINTER_ENTITY_KEY = "jgEntityId";
|
|
4
|
+
export declare const POINTER_OBJECT_KEY = "jgObjectId";
|
|
5
|
+
interface PointerDeps {
|
|
6
|
+
camera: THREE.Camera;
|
|
7
|
+
scene: THREE.Scene;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
}
|
|
11
|
+
export interface PointerService {
|
|
12
|
+
/** Cast the current cursor into the world; null when the cursor is off-canvas. */
|
|
13
|
+
worldHit(): PointerHit | null;
|
|
14
|
+
/** Cast from the viewport center regardless of cursor presence (pointer-lock aim); null before the probe binds. */
|
|
15
|
+
worldHitCenter(): PointerHit | null;
|
|
16
|
+
/** Project a world point to CSS pixels for the marquee / HUD; null before the probe binds. */
|
|
17
|
+
screenOf(world: PointerVec3): {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
} | null;
|
|
21
|
+
hasCursor(): boolean;
|
|
22
|
+
bind(deps: PointerDeps | null): void;
|
|
23
|
+
setCursor(ndcX: number, ndcY: number, present: boolean): void;
|
|
24
|
+
}
|
|
25
|
+
export declare function createPointerService(): PointerService;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
export const POINTER_ENTITY_KEY = "jgEntityId";
|
|
3
|
+
export const POINTER_OBJECT_KEY = "jgObjectId";
|
|
4
|
+
function tagOf(object, key) {
|
|
5
|
+
let cursor = object;
|
|
6
|
+
while (cursor !== null) {
|
|
7
|
+
const value = cursor.userData[key];
|
|
8
|
+
if (typeof value === "string")
|
|
9
|
+
return value;
|
|
10
|
+
cursor = cursor.parent;
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
function standardMaterialSample(material) {
|
|
15
|
+
const candidates = Array.isArray(material) ? material : [material];
|
|
16
|
+
const standard = candidates.find((entry) => entry instanceof THREE.MeshStandardMaterial);
|
|
17
|
+
if (standard === undefined)
|
|
18
|
+
return null;
|
|
19
|
+
return { color: `#${standard.color.getHexString()}`, metalness: standard.metalness, roughness: standard.roughness };
|
|
20
|
+
}
|
|
21
|
+
export function createPointerService() {
|
|
22
|
+
const raycaster = new THREE.Raycaster();
|
|
23
|
+
const ndc = new THREE.Vector2();
|
|
24
|
+
const centerNdc = new THREE.Vector2(0, 0);
|
|
25
|
+
const scratch = new THREE.Vector3();
|
|
26
|
+
const normalMatrix = new THREE.Matrix3();
|
|
27
|
+
const groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
|
|
28
|
+
let deps = null;
|
|
29
|
+
let cursorPresent = false;
|
|
30
|
+
function hitAtNdc(target) {
|
|
31
|
+
if (deps === null)
|
|
32
|
+
return null;
|
|
33
|
+
raycaster.setFromCamera(target, deps.camera);
|
|
34
|
+
const intersects = raycaster.intersectObjects(deps.scene.children, true);
|
|
35
|
+
for (const hit of intersects) {
|
|
36
|
+
const mesh = hit.object;
|
|
37
|
+
if (!mesh.isMesh)
|
|
38
|
+
continue;
|
|
39
|
+
const point = [hit.point.x, hit.point.y, hit.point.z];
|
|
40
|
+
let normal = [0, 1, 0];
|
|
41
|
+
if (hit.face !== null && hit.face !== undefined) {
|
|
42
|
+
normalMatrix.getNormalMatrix(hit.object.matrixWorld);
|
|
43
|
+
scratch.copy(hit.face.normal).applyMatrix3(normalMatrix).normalize();
|
|
44
|
+
normal = [scratch.x, scratch.y, scratch.z];
|
|
45
|
+
}
|
|
46
|
+
const uv = hit.uv !== undefined ? { u: hit.uv.x, v: hit.uv.y } : undefined;
|
|
47
|
+
return {
|
|
48
|
+
point,
|
|
49
|
+
normal,
|
|
50
|
+
entity: tagOf(hit.object, POINTER_ENTITY_KEY),
|
|
51
|
+
object: tagOf(hit.object, POINTER_OBJECT_KEY),
|
|
52
|
+
...(uv !== undefined ? { uv } : {}),
|
|
53
|
+
material: standardMaterialSample(mesh.material),
|
|
54
|
+
...(hit.instanceId !== undefined ? { instanceId: hit.instanceId } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const grounded = raycaster.ray.intersectPlane(groundPlane, scratch);
|
|
58
|
+
if (grounded === null)
|
|
59
|
+
return null;
|
|
60
|
+
return { point: [grounded.x, grounded.y, grounded.z], normal: [0, 1, 0], entity: null, object: null };
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
hasCursor: () => cursorPresent,
|
|
64
|
+
bind: (next) => {
|
|
65
|
+
deps = next;
|
|
66
|
+
},
|
|
67
|
+
setCursor: (ndcX, ndcY, present) => {
|
|
68
|
+
ndc.set(ndcX, ndcY);
|
|
69
|
+
cursorPresent = present;
|
|
70
|
+
},
|
|
71
|
+
worldHit() {
|
|
72
|
+
if (!cursorPresent)
|
|
73
|
+
return null;
|
|
74
|
+
return hitAtNdc(ndc);
|
|
75
|
+
},
|
|
76
|
+
worldHitCenter() {
|
|
77
|
+
return hitAtNdc(centerNdc);
|
|
78
|
+
},
|
|
79
|
+
screenOf(world) {
|
|
80
|
+
if (deps === null)
|
|
81
|
+
return null;
|
|
82
|
+
scratch.set(world[0], world[1], world[2]).project(deps.camera);
|
|
83
|
+
return {
|
|
84
|
+
x: (scratch.x * 0.5 + 0.5) * deps.width,
|
|
85
|
+
y: (-scratch.y * 0.5 + 0.5) * deps.height,
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
package/dist/registry.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ComponentType, ReactNode } from "react";
|
|
2
2
|
import type { PlayableGame as EnginePlayableGame } from "@jgengine/core/game/playableGame";
|
|
3
3
|
import type { SceneEntity } from "@jgengine/core/scene/entityStore";
|
|
4
|
+
import type { SceneObject } from "@jgengine/core/scene/objectStore";
|
|
4
5
|
export type RenderEntity = (entity: SceneEntity) => ReactNode;
|
|
5
|
-
export type
|
|
6
|
+
export type RenderObject = (object: SceneObject) => ReactNode;
|
|
7
|
+
export type PlayableGame = EnginePlayableGame<ComponentType, ComponentType, RenderEntity, RenderObject>;
|
|
6
8
|
export type GameRegistry = Record<string, () => Promise<PlayableGame>>;
|
|
9
|
+
export declare function resolveGameLoader(registry: GameRegistry, gameId: string, fallbackGameId?: string): (() => Promise<PlayableGame>) | undefined;
|
package/dist/registry.js
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import type { PaintStroke } from "@jgengine/core/scene/paintLayer";
|
|
3
|
+
export declare const PAINT_TEXTURE_SIZE = 512;
|
|
4
|
+
export declare function cloneModelScene(source: THREE.Object3D): THREE.Object3D;
|
|
5
|
+
export declare function standardMaterialsOf(root: THREE.Object3D): THREE.MeshStandardMaterial[];
|
|
6
|
+
export interface PaintCanvas {
|
|
7
|
+
canvas: HTMLCanvasElement;
|
|
8
|
+
context: CanvasRenderingContext2D;
|
|
9
|
+
texture: THREE.CanvasTexture;
|
|
10
|
+
}
|
|
11
|
+
export declare function createPaintCanvas(seed: THREE.MeshStandardMaterial, size?: number): PaintCanvas;
|
|
12
|
+
export declare function drawPaintStrokes(paint: PaintCanvas, strokes: readonly PaintStroke[]): void;
|
|
13
|
+
export declare function applyPaintTexture(root: THREE.Object3D, paint: PaintCanvas): void;
|
|
14
|
+
export declare function syncPaintCanvas(paint: PaintCanvas, seedColor: THREE.Color, strokes: readonly PaintStroke[], drawnCount: number): number;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { clone as cloneSkinned } from "three/examples/jsm/utils/SkeletonUtils.js";
|
|
3
|
+
export const PAINT_TEXTURE_SIZE = 512;
|
|
4
|
+
export function cloneModelScene(source) {
|
|
5
|
+
const clone = cloneSkinned(source);
|
|
6
|
+
clone.traverse((node) => {
|
|
7
|
+
const mesh = node;
|
|
8
|
+
if (!mesh.isMesh)
|
|
9
|
+
return;
|
|
10
|
+
mesh.material = Array.isArray(mesh.material)
|
|
11
|
+
? mesh.material.map((material) => material.clone())
|
|
12
|
+
: mesh.material.clone();
|
|
13
|
+
});
|
|
14
|
+
return clone;
|
|
15
|
+
}
|
|
16
|
+
function isMeshStandardMaterial(material) {
|
|
17
|
+
return material.isMeshStandardMaterial === true;
|
|
18
|
+
}
|
|
19
|
+
export function standardMaterialsOf(root) {
|
|
20
|
+
const materials = [];
|
|
21
|
+
root.traverse((node) => {
|
|
22
|
+
const mesh = node;
|
|
23
|
+
if (!mesh.isMesh)
|
|
24
|
+
return;
|
|
25
|
+
const list = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
26
|
+
for (const material of list)
|
|
27
|
+
if (isMeshStandardMaterial(material))
|
|
28
|
+
materials.push(material);
|
|
29
|
+
});
|
|
30
|
+
return materials;
|
|
31
|
+
}
|
|
32
|
+
export function createPaintCanvas(seed, size = PAINT_TEXTURE_SIZE) {
|
|
33
|
+
const canvas = document.createElement("canvas");
|
|
34
|
+
canvas.width = size;
|
|
35
|
+
canvas.height = size;
|
|
36
|
+
const context = canvas.getContext("2d");
|
|
37
|
+
const seedImage = seed.map?.image;
|
|
38
|
+
if (seedImage !== undefined && seedImage.width) {
|
|
39
|
+
context.drawImage(seedImage, 0, 0, size, size);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
context.fillStyle = `#${seed.color.getHexString()}`;
|
|
43
|
+
context.fillRect(0, 0, size, size);
|
|
44
|
+
}
|
|
45
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
46
|
+
texture.colorSpace = seed.map?.colorSpace ?? THREE.SRGBColorSpace;
|
|
47
|
+
return { canvas, context, texture };
|
|
48
|
+
}
|
|
49
|
+
export function drawPaintStrokes(paint, strokes) {
|
|
50
|
+
const { canvas, context, texture } = paint;
|
|
51
|
+
for (const stroke of strokes) {
|
|
52
|
+
context.fillStyle = stroke.color;
|
|
53
|
+
context.beginPath();
|
|
54
|
+
context.arc(stroke.u * canvas.width, (1 - stroke.v) * canvas.height, stroke.radius * canvas.width, 0, Math.PI * 2);
|
|
55
|
+
context.fill();
|
|
56
|
+
}
|
|
57
|
+
texture.needsUpdate = true;
|
|
58
|
+
}
|
|
59
|
+
export function applyPaintTexture(root, paint) {
|
|
60
|
+
for (const material of standardMaterialsOf(root)) {
|
|
61
|
+
material.map = paint.texture;
|
|
62
|
+
material.needsUpdate = true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function syncPaintCanvas(paint, seedColor, strokes, drawnCount) {
|
|
66
|
+
if (strokes.length < drawnCount) {
|
|
67
|
+
const { canvas, context } = paint;
|
|
68
|
+
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
69
|
+
context.fillStyle = `#${seedColor.getHexString()}`;
|
|
70
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
71
|
+
drawPaintStrokes(paint, strokes);
|
|
72
|
+
return strokes.length;
|
|
73
|
+
}
|
|
74
|
+
if (strokes.length > drawnCount) {
|
|
75
|
+
drawPaintStrokes(paint, strokes.slice(drawnCount));
|
|
76
|
+
return strokes.length;
|
|
77
|
+
}
|
|
78
|
+
return drawnCount;
|
|
79
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type RecordingBuffer, type RecordingBufferOptions } from "@jgengine/core/sensor/recordingBuffer";
|
|
2
|
+
import type { EntityPosition } from "@jgengine/core/scene/entityStore";
|
|
3
|
+
export interface RecordedPose {
|
|
4
|
+
position: EntityPosition;
|
|
5
|
+
rotationY: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Session-recording buffer (#120) for replay / photo mode / kill-cam: records
|
|
9
|
+
* an entity's pose on game-time every frame into a `RecordingBuffer`, which a
|
|
10
|
+
* game can then `seek()` to scrub, drive an observer cam ghost, or export a
|
|
11
|
+
* kill-cam clip. Recording rides on `ctx.time.now()`, so pause/fast-forward
|
|
12
|
+
* scrub the recording exactly like the live sim.
|
|
13
|
+
*/
|
|
14
|
+
export declare function useSessionRecorder(entityId: string, options?: RecordingBufferOptions): RecordingBuffer<RecordedPose>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useFrame } from "@react-three/fiber";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { createRecordingBuffer } from "@jgengine/core/sensor/recordingBuffer";
|
|
4
|
+
import { useGameContext } from "@jgengine/react/provider";
|
|
5
|
+
/**
|
|
6
|
+
* Session-recording buffer (#120) for replay / photo mode / kill-cam: records
|
|
7
|
+
* an entity's pose on game-time every frame into a `RecordingBuffer`, which a
|
|
8
|
+
* game can then `seek()` to scrub, drive an observer cam ghost, or export a
|
|
9
|
+
* kill-cam clip. Recording rides on `ctx.time.now()`, so pause/fast-forward
|
|
10
|
+
* scrub the recording exactly like the live sim.
|
|
11
|
+
*/
|
|
12
|
+
export function useSessionRecorder(entityId, options) {
|
|
13
|
+
const ctx = useGameContext();
|
|
14
|
+
const buffer = useMemo(() => createRecordingBuffer(options), []);
|
|
15
|
+
useFrame(() => {
|
|
16
|
+
const entity = ctx.scene.entity.get(entityId);
|
|
17
|
+
if (entity === null)
|
|
18
|
+
return;
|
|
19
|
+
buffer.append(ctx.time.now(), { position: entity.position, rotationY: entity.rotationY });
|
|
20
|
+
});
|
|
21
|
+
return buffer;
|
|
22
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PlacementPreview } from "@jgengine/core/world/placementController";
|
|
2
|
+
export interface PlacementGhostProps {
|
|
3
|
+
preview: PlacementPreview | null;
|
|
4
|
+
height?: number;
|
|
5
|
+
validColor?: string;
|
|
6
|
+
invalidColor?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function PlacementGhost({ preview, height, validColor, invalidColor, }: PlacementGhostProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { quarterTurnsToRotationY } from "@jgengine/core/world/placementController";
|
|
4
|
+
export function PlacementGhost({ preview, height = 1, validColor = "#34d399", invalidColor = "#f87171", }) {
|
|
5
|
+
const footprint = preview?.footprint;
|
|
6
|
+
const size = useMemo(() => [footprint?.w ?? 1, height, footprint?.d ?? 1], [footprint?.w, footprint?.d, height]);
|
|
7
|
+
if (preview === null)
|
|
8
|
+
return null;
|
|
9
|
+
const color = preview.valid ? validColor : invalidColor;
|
|
10
|
+
return (_jsxs("group", { position: [preview.center[0], preview.y, preview.center[1]], "rotation-y": quarterTurnsToRotationY(preview.quarterTurns), children: [_jsxs("mesh", { "position-y": height / 2, children: [_jsx("boxGeometry", { args: size }), _jsx("meshStandardMaterial", { color: color, transparent: true, opacity: 0.35, depthWrite: false, emissive: color, emissiveIntensity: 0.4 })] }), _jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.02, children: [_jsx("ringGeometry", { args: [Math.max(size[0], size[2]) * 0.5, Math.max(size[0], size[2]) * 0.5 + 0.12, 40] }), _jsx("meshBasicMaterial", { color: color, transparent: true, opacity: 0.85 })] })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./structures/index.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./structures/index.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ThreeElements } from "@react-three/fiber";
|
|
2
|
+
import type { TerrainField } from "@jgengine/core/world/terrain";
|
|
3
|
+
import { type FieldGroundOptions } from "./terrainMath.js";
|
|
4
|
+
export interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material"> {
|
|
5
|
+
/** The terrain field to mesh — a `CarvableField` renders its craters and mounds. */
|
|
6
|
+
field: TerrainField;
|
|
7
|
+
size?: FieldGroundOptions["size"];
|
|
8
|
+
segments?: FieldGroundOptions["segments"];
|
|
9
|
+
center?: FieldGroundOptions["center"];
|
|
10
|
+
colors?: FieldGroundOptions["colors"];
|
|
11
|
+
heightRange?: FieldGroundOptions["heightRange"];
|
|
12
|
+
roughness?: number;
|
|
13
|
+
metalness?: number;
|
|
14
|
+
/** Bump after a runtime carve/deposit to re-mesh the deformed surface. */
|
|
15
|
+
epoch?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Renders a `TerrainField` as a deformed ground mesh — the crater/mound view for destructible terrain.
|
|
19
|
+
* Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl
|
|
20
|
+
* once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
|
|
21
|
+
*/
|
|
22
|
+
export declare function CarvedTerrain({ field, size, segments, center, colors, heightRange, roughness, metalness, receiveShadow, epoch, ...meshProps }: CarvedTerrainProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import {} from "@react-three/fiber";
|
|
3
|
+
import { useEffect, useMemo } from "react";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { createFieldGroundGeometry } from "./terrainMath.js";
|
|
6
|
+
/**
|
|
7
|
+
* Renders a `TerrainField` as a deformed ground mesh — the crater/mound view for destructible terrain.
|
|
8
|
+
* Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl
|
|
9
|
+
* once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
|
|
10
|
+
*/
|
|
11
|
+
export function CarvedTerrain({ field, size, segments, center, colors, heightRange, roughness = 0.95, metalness = 0, receiveShadow = true, epoch = 0, ...meshProps }) {
|
|
12
|
+
const geometry = useMemo(() => createFieldGroundGeometry(field, { size, segments, center, colors, heightRange }),
|
|
13
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
14
|
+
[field, size, segments, center, colors, heightRange, epoch]);
|
|
15
|
+
const material = useMemo(() => new THREE.MeshStandardMaterial({ color: "#ffffff", roughness, metalness, vertexColors: true }), [metalness, roughness]);
|
|
16
|
+
useEffect(() => () => geometry.dispose(), [geometry]);
|
|
17
|
+
useEffect(() => () => material.dispose(), [material]);
|
|
18
|
+
return _jsx("mesh", { ...meshProps, geometry: geometry, material: material, receiveShadow: receiveShadow });
|
|
19
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Aabb } from "@jgengine/core/world/geometry";
|
|
2
|
+
import type { EditableTerrain } from "@jgengine/core/world/terraform";
|
|
3
|
+
export interface EditableGroundProps {
|
|
4
|
+
terrain: Pick<EditableTerrain, "sampleHeight" | "surfaceAt">;
|
|
5
|
+
bounds: Aabb;
|
|
6
|
+
segments?: number;
|
|
7
|
+
version?: number;
|
|
8
|
+
baseColor?: string;
|
|
9
|
+
surfaceColors?: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
export declare function EditableGround({ terrain, bounds, segments, version, baseColor, surfaceColors, }: EditableGroundProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import * as THREE from "three";
|
|
4
|
+
const DEFAULT_SURFACE_COLORS = {
|
|
5
|
+
path: "#b8a06a",
|
|
6
|
+
gravel: "#8d8d84",
|
|
7
|
+
stone: "#7c7f86",
|
|
8
|
+
grass: "#4b7f3f",
|
|
9
|
+
};
|
|
10
|
+
export function EditableGround({ terrain, bounds, segments = 96, version = 0, baseColor = "#3f6b3a", surfaceColors = DEFAULT_SURFACE_COLORS, }) {
|
|
11
|
+
const geometry = useMemo(() => {
|
|
12
|
+
void version;
|
|
13
|
+
const width = bounds.maxX - bounds.minX;
|
|
14
|
+
const depth = bounds.maxZ - bounds.minZ;
|
|
15
|
+
const geo = new THREE.PlaneGeometry(width, depth, segments, segments);
|
|
16
|
+
geo.rotateX(-Math.PI / 2);
|
|
17
|
+
const position = geo.attributes.position;
|
|
18
|
+
const base = new THREE.Color(baseColor);
|
|
19
|
+
const colors = new Float32Array(position.count * 3);
|
|
20
|
+
const cx = (bounds.minX + bounds.maxX) / 2;
|
|
21
|
+
const cz = (bounds.minZ + bounds.maxZ) / 2;
|
|
22
|
+
for (let index = 0; index < position.count; index += 1) {
|
|
23
|
+
const x = position.getX(index) + cx;
|
|
24
|
+
const z = position.getZ(index) + cz;
|
|
25
|
+
position.setY(index, terrain.sampleHeight(x, z));
|
|
26
|
+
const surface = terrain.surfaceAt(x, z);
|
|
27
|
+
const color = surface !== null && surfaceColors[surface] !== undefined ? new THREE.Color(surfaceColors[surface]) : base;
|
|
28
|
+
colors[index * 3] = color.r;
|
|
29
|
+
colors[index * 3 + 1] = color.g;
|
|
30
|
+
colors[index * 3 + 2] = color.b;
|
|
31
|
+
}
|
|
32
|
+
geo.setAttribute("color", new THREE.BufferAttribute(colors, 3));
|
|
33
|
+
geo.computeVertexNormals();
|
|
34
|
+
return geo;
|
|
35
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
36
|
+
}, [terrain, bounds, segments, version, baseColor, surfaceColors]);
|
|
37
|
+
return (_jsx("mesh", { geometry: geometry, receiveShadow: true, children: _jsx("meshStandardMaterial", { vertexColors: true, roughness: 0.95, metalness: 0 }) }));
|
|
38
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TerraformMode } from "@jgengine/core/world/terraform";
|
|
2
|
+
export interface TerraformBrushCursorProps {
|
|
3
|
+
center: readonly [number, number] | null;
|
|
4
|
+
y?: number;
|
|
5
|
+
radius: number;
|
|
6
|
+
mode: TerraformMode;
|
|
7
|
+
}
|
|
8
|
+
export declare function TerraformBrushCursor({ center, y, radius, mode }: TerraformBrushCursorProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
const MODE_COLOR = {
|
|
3
|
+
raise: "#38bdf8",
|
|
4
|
+
lower: "#fb923c",
|
|
5
|
+
flatten: "#a3e635",
|
|
6
|
+
paint: "#e879f9",
|
|
7
|
+
};
|
|
8
|
+
export function TerraformBrushCursor({ center, y = 0.05, radius, mode }) {
|
|
9
|
+
if (center === null)
|
|
10
|
+
return null;
|
|
11
|
+
const color = MODE_COLOR[mode];
|
|
12
|
+
return (_jsxs("group", { position: [center[0], y, center[1]], children: [_jsxs("mesh", { "rotation-x": -Math.PI / 2, children: [_jsx("ringGeometry", { args: [radius - 0.15, radius, 48] }), _jsx("meshBasicMaterial", { color: color, transparent: true, opacity: 0.9 })] }), _jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": -0.02, children: [_jsx("circleGeometry", { args: [radius, 48] }), _jsx("meshBasicMaterial", { color: color, transparent: true, opacity: 0.14, depthWrite: false })] })] }));
|
|
13
|
+
}
|
package/dist/terrain/index.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
export { CarvedTerrain, type CarvedTerrainProps } from "./CarvedTerrain.js";
|
|
1
2
|
export { GrassField, type GrassFieldProps } from "./GrassField.js";
|
|
2
3
|
export { ProceduralGround, type ProceduralGroundProps } from "./ProceduralGround.js";
|
|
4
|
+
export { EditableGround, type EditableGroundProps } from "./EditableGround.js";
|
|
5
|
+
export { TerraformBrushCursor, type TerraformBrushCursorProps } from "./TerraformBrushCursor.js";
|
|
3
6
|
export { createGrassBladeGeometry, resolveGrassBladeGeometryOptions, resolveGrassRange, type GrassBladeGeometryOptions, type GrassRange, type ResolvedGrassBladeGeometryOptions, } from "./grassGeometry.js";
|
|
4
7
|
export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, type GrassMaterialHandle, type GrassMaterialOptions, type GrassShaderUniforms, type GrassWindOptions, } from "./grassMaterial.js";
|
|
5
8
|
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";
|
|
9
|
+
export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, normalizeHeightBlend, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, type FieldGroundOptions, type ProceduralTerrainConfig, type ResolvedTerrainSegments, type ResolvedTerrainSize, type TerrainArea, type TerrainHeightSampler, type TerrainVertexColorOptions, } from "./terrainMath.js";
|
|
7
10
|
export { arenaField, flatField, fractalNoise, noiseField, resolveGroundStep, resolveTerrainField, valueNoise, withNormal, type FractalNoiseConfig, type NoiseFieldConfig, type TerrainField, type TerrainNormal, } from "@jgengine/core/world/terrain";
|
package/dist/terrain/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
export { CarvedTerrain } from "./CarvedTerrain.js";
|
|
1
2
|
export { GrassField } from "./GrassField.js";
|
|
2
3
|
export { ProceduralGround } from "./ProceduralGround.js";
|
|
4
|
+
export { EditableGround } from "./EditableGround.js";
|
|
5
|
+
export { TerraformBrushCursor } from "./TerraformBrushCursor.js";
|
|
3
6
|
export { createGrassBladeGeometry, resolveGrassBladeGeometryOptions, resolveGrassRange, } from "./grassGeometry.js";
|
|
4
7
|
export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, } from "./grassMaterial.js";
|
|
5
8
|
export { createSeededRandom, hashNoise2, seedToUint32 } from "./random.js";
|
|
6
|
-
export { createProceduralGroundGeometry, createProceduralTerrainSampler, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, } from "./terrainMath.js";
|
|
9
|
+
export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, normalizeHeightBlend, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, } from "./terrainMath.js";
|
|
7
10
|
export { arenaField, flatField, fractalNoise, noiseField, resolveGroundStep, resolveTerrainField, valueNoise, withNormal, } from "@jgengine/core/world/terrain";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type NoiseFieldConfig } from "@jgengine/core/world/terrain";
|
|
1
|
+
import { type NoiseFieldConfig, type TerrainField } from "@jgengine/core/world/terrain";
|
|
2
2
|
import * as THREE from "three";
|
|
3
3
|
import { type TerrainSeed } from "./random.js";
|
|
4
4
|
export type TerrainArea = number | readonly [width: number, depth: number];
|
|
@@ -27,8 +27,22 @@ export interface TerrainVertexColorOptions {
|
|
|
27
27
|
waterline?: THREE.ColorRepresentation;
|
|
28
28
|
waterlineHeight?: number;
|
|
29
29
|
}
|
|
30
|
+
export declare function normalizeHeightBlend(height: number, minHeight: number, maxHeight: number): number;
|
|
30
31
|
export declare function resolveTerrainSize(size?: TerrainArea): ResolvedTerrainSize;
|
|
31
32
|
export declare function resolveTerrainSegments(segments?: ProceduralTerrainConfig["segments"]): ResolvedTerrainSegments;
|
|
32
33
|
export declare function toNoiseFieldConfig(config?: ProceduralTerrainConfig): NoiseFieldConfig;
|
|
33
34
|
export declare function createProceduralTerrainSampler(config?: ProceduralTerrainConfig): TerrainHeightSampler;
|
|
35
|
+
export interface FieldGroundOptions {
|
|
36
|
+
size?: TerrainArea;
|
|
37
|
+
segments?: number | readonly [x: number, z: number];
|
|
38
|
+
center?: readonly [x: number, z: number];
|
|
39
|
+
colors?: TerrainVertexColorOptions;
|
|
40
|
+
heightRange?: readonly [min: number, max: number];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Mesh any `TerrainField` — including a `CarvableField` with craters/mounds written into it — into a
|
|
44
|
+
* vertex-coloured ground geometry. `sampleHeight` drives the vertices, so runtime carves show up as
|
|
45
|
+
* real depressions the moment the field is re-sampled (bump the caller's rebuild key after a carve).
|
|
46
|
+
*/
|
|
47
|
+
export declare function createFieldGroundGeometry(field: TerrainField, options?: FieldGroundOptions): THREE.BufferGeometry;
|
|
34
48
|
export declare function createProceduralGroundGeometry(config?: ProceduralTerrainConfig, colors?: TerrainVertexColorOptions): THREE.BufferGeometry;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { noiseField } from "@jgengine/core/world/terrain";
|
|
2
2
|
import * as THREE from "three";
|
|
3
3
|
import {} from "./random.js";
|
|
4
|
+
export function normalizeHeightBlend(height, minHeight, maxHeight) {
|
|
5
|
+
const range = maxHeight - minHeight;
|
|
6
|
+
if (range <= 1e-6)
|
|
7
|
+
return 0.5;
|
|
8
|
+
return THREE.MathUtils.clamp((height - minHeight) / range, 0, 1);
|
|
9
|
+
}
|
|
4
10
|
export function resolveTerrainSize(size = 40) {
|
|
5
11
|
return typeof size === "number" ? { width: size, depth: size } : { width: size[0], depth: size[1] };
|
|
6
12
|
}
|
|
@@ -23,6 +29,86 @@ export function toNoiseFieldConfig(config = {}) {
|
|
|
23
29
|
export function createProceduralTerrainSampler(config = {}) {
|
|
24
30
|
return noiseField(toNoiseFieldConfig(config)).sampleHeight;
|
|
25
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Mesh any `TerrainField` — including a `CarvableField` with craters/mounds written into it — into a
|
|
34
|
+
* vertex-coloured ground geometry. `sampleHeight` drives the vertices, so runtime carves show up as
|
|
35
|
+
* real depressions the moment the field is re-sampled (bump the caller's rebuild key after a carve).
|
|
36
|
+
*/
|
|
37
|
+
export function createFieldGroundGeometry(field, options = {}) {
|
|
38
|
+
const size = resolveTerrainSize(options.size);
|
|
39
|
+
const segments = resolveTerrainSegments(options.segments);
|
|
40
|
+
const [cx, cz] = options.center ?? [0, 0];
|
|
41
|
+
return buildGroundGeometry(size, segments, (x, z) => field.sampleHeight(x, z), {
|
|
42
|
+
colors: options.colors ?? {},
|
|
43
|
+
center: [cx, cz],
|
|
44
|
+
heightRange: options.heightRange,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function buildGroundGeometry(size, segments, sampler, opts) {
|
|
48
|
+
const colors = opts.colors;
|
|
49
|
+
const vertexCountX = segments.x + 1;
|
|
50
|
+
const vertexCountZ = segments.z + 1;
|
|
51
|
+
const positions = new Float32Array(vertexCountX * vertexCountZ * 3);
|
|
52
|
+
const uvs = new Float32Array(vertexCountX * vertexCountZ * 2);
|
|
53
|
+
const colorValues = new Float32Array(vertexCountX * vertexCountZ * 3);
|
|
54
|
+
const indices = new Uint32Array(segments.x * segments.z * 6);
|
|
55
|
+
const low = new THREE.Color(colors.low ?? "#30402c");
|
|
56
|
+
const high = new THREE.Color(colors.high ?? "#7f8b50");
|
|
57
|
+
const waterline = colors.waterline === undefined ? null : new THREE.Color(colors.waterline);
|
|
58
|
+
const minHeight = opts.heightRange?.[0] ?? -3;
|
|
59
|
+
const maxHeight = opts.heightRange?.[1] ?? 3;
|
|
60
|
+
let index = 0;
|
|
61
|
+
let uvIndex = 0;
|
|
62
|
+
let colorIndex = 0;
|
|
63
|
+
for (let zIndex = 0; zIndex < vertexCountZ; zIndex += 1) {
|
|
64
|
+
const v = zIndex / segments.z;
|
|
65
|
+
const z = opts.center[1] + (v - 0.5) * size.depth;
|
|
66
|
+
for (let xIndex = 0; xIndex < vertexCountX; xIndex += 1) {
|
|
67
|
+
const u = xIndex / segments.x;
|
|
68
|
+
const x = opts.center[0] + (u - 0.5) * size.width;
|
|
69
|
+
const y = sampler(x, z);
|
|
70
|
+
positions[index] = x;
|
|
71
|
+
positions[index + 1] = y;
|
|
72
|
+
positions[index + 2] = z;
|
|
73
|
+
uvs[uvIndex] = u;
|
|
74
|
+
uvs[uvIndex + 1] = v;
|
|
75
|
+
const blend = normalizeHeightBlend(y, minHeight, maxHeight);
|
|
76
|
+
const color = low.clone().lerp(high, blend);
|
|
77
|
+
if (waterline !== null && y <= (colors.waterlineHeight ?? 0))
|
|
78
|
+
color.lerp(waterline, 0.65);
|
|
79
|
+
colorValues[colorIndex] = color.r;
|
|
80
|
+
colorValues[colorIndex + 1] = color.g;
|
|
81
|
+
colorValues[colorIndex + 2] = color.b;
|
|
82
|
+
index += 3;
|
|
83
|
+
uvIndex += 2;
|
|
84
|
+
colorIndex += 3;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
let triangleIndex = 0;
|
|
88
|
+
for (let zIndex = 0; zIndex < segments.z; zIndex += 1) {
|
|
89
|
+
for (let xIndex = 0; xIndex < segments.x; xIndex += 1) {
|
|
90
|
+
const a = zIndex * vertexCountX + xIndex;
|
|
91
|
+
const b = a + 1;
|
|
92
|
+
const c = a + vertexCountX;
|
|
93
|
+
const d = c + 1;
|
|
94
|
+
indices[triangleIndex] = a;
|
|
95
|
+
indices[triangleIndex + 1] = c;
|
|
96
|
+
indices[triangleIndex + 2] = b;
|
|
97
|
+
indices[triangleIndex + 3] = b;
|
|
98
|
+
indices[triangleIndex + 4] = c;
|
|
99
|
+
indices[triangleIndex + 5] = d;
|
|
100
|
+
triangleIndex += 6;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const geometry = new THREE.BufferGeometry();
|
|
104
|
+
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
|
105
|
+
geometry.setAttribute("uv", new THREE.BufferAttribute(uvs, 2));
|
|
106
|
+
geometry.setAttribute("color", new THREE.BufferAttribute(colorValues, 3));
|
|
107
|
+
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
|
|
108
|
+
geometry.computeVertexNormals();
|
|
109
|
+
geometry.computeBoundingSphere();
|
|
110
|
+
return geometry;
|
|
111
|
+
}
|
|
26
112
|
export function createProceduralGroundGeometry(config = {}, colors = {}) {
|
|
27
113
|
const size = resolveTerrainSize(config.size);
|
|
28
114
|
const segments = resolveTerrainSegments(config.segments);
|
|
@@ -53,7 +139,7 @@ export function createProceduralGroundGeometry(config = {}, colors = {}) {
|
|
|
53
139
|
positions[index + 2] = z;
|
|
54
140
|
uvs[uvIndex] = u;
|
|
55
141
|
uvs[uvIndex + 1] = v;
|
|
56
|
-
const blend =
|
|
142
|
+
const blend = normalizeHeightBlend(y, minHeight, maxHeight);
|
|
57
143
|
const color = low.clone().lerp(high, blend);
|
|
58
144
|
if (waterline !== null && y <= (colors.waterlineHeight ?? 0))
|
|
59
145
|
color.lerp(waterline, 0.65);
|