@jgengine/shell 0.5.0 → 0.7.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 +191 -2
- package/README.md +27 -0
- package/dist/GamePlayerShell.js +224 -43
- 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/camera/GameCameraRig.d.ts +11 -0
- package/dist/camera/GameCameraRig.js +42 -0
- package/dist/camera/GameOrbitCamera.d.ts +3 -1
- package/dist/camera/GameOrbitCamera.js +4 -2
- package/dist/camera/cameraRigs.d.ts +22 -0
- package/dist/camera/cameraRigs.js +485 -0
- package/dist/camera/index.d.ts +5 -0
- package/dist/camera/index.js +5 -0
- package/dist/camera/rigMath.d.ts +130 -0
- package/dist/camera/rigMath.js +338 -0
- package/dist/camera/shakeChannel.d.ts +29 -0
- package/dist/camera/shakeChannel.js +38 -0
- package/dist/demo/builderDemo.d.ts +2 -0
- package/dist/demo/builderDemo.js +189 -0
- package/dist/demo/demoGame.js +10 -2
- package/dist/demo/environmentShowcase.d.ts +2 -0
- package/dist/demo/environmentShowcase.js +15 -0
- package/dist/demo/mapDemo.d.ts +2 -0
- package/dist/demo/mapDemo.js +206 -0
- package/dist/demo/pointerDemo.d.ts +2 -0
- package/dist/demo/pointerDemo.js +151 -0
- package/dist/demo/sensorShowcase.d.ts +2 -0
- package/dist/demo/sensorShowcase.js +131 -0
- package/dist/demo/survivalDemo.d.ts +2 -0
- package/dist/demo/survivalDemo.js +291 -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/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/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 +27 -0
- package/dist/pointer/pointerService.d.ts +24 -0
- package/dist/pointer/pointerService.js +68 -0
- package/dist/registry.d.ts +4 -2
- package/dist/replay/useSessionRecorder.d.ts +14 -0
- package/dist/replay/useSessionRecorder.js +22 -0
- package/dist/structures/GeneratedBuilding.d.ts +44 -0
- package/dist/structures/GeneratedBuilding.js +86 -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/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/TerraformBrushCursor.d.ts +8 -0
- package/dist/terrain/TerraformBrushCursor.js +13 -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 +10 -0
- package/dist/terrain/index.js +10 -0
- package/dist/terrain/random.d.ts +4 -0
- package/dist/terrain/random.js +27 -0
- package/dist/terrain/terrainMath.d.ts +47 -0
- package/dist/terrain/terrainMath.js +172 -0
- package/dist/terrain.d.ts +1 -0
- package/dist/terrain.js +1 -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/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/FireSpreadLayer.d.ts +14 -0
- package/dist/weather/FireSpreadLayer.js +68 -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 +6 -0
- package/dist/weather/index.js +6 -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/dist/world/InstancedJoints.d.ts +14 -0
- package/dist/world/InstancedJoints.js +32 -0
- package/dist/world/WorldHud.d.ts +2 -0
- package/dist/world/WorldHud.js +70 -12
- package/dist/world/WorldItems.d.ts +5 -0
- package/dist/world/WorldItems.js +31 -0
- package/dist/world/floatTextStyle.d.ts +14 -0
- package/dist/world/floatTextStyle.js +39 -0
- package/package.json +12 -4
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { probeHiddenState } from "@jgengine/core/sensor/hiddenStateProbe";
|
|
3
|
+
/** Reads a hidden zone/entity state variable in range (EMF / thermometer / geiger style sensor verb, #116). */
|
|
4
|
+
export function useHiddenStateProbe(origin, sources, options) {
|
|
5
|
+
return probeHiddenState(origin, sources, options);
|
|
6
|
+
}
|
|
7
|
+
/** A handheld-sensor readout: needle strength bar + the raw reading, or a "no signal" idle state. */
|
|
8
|
+
export function SensorReadoutMeter({ label, reading, className }) {
|
|
9
|
+
const percent = reading === null ? 0 : Math.round(reading.strength * 100);
|
|
10
|
+
return (_jsxs("div", { className: className ?? "w-44 rounded border border-emerald-400/30 bg-black/70 p-2 text-xs text-emerald-200 shadow-lg", children: [_jsxs("div", { className: "mb-1 flex items-center justify-between uppercase tracking-wide", children: [_jsx("span", { children: label }), _jsx("span", { className: "tabular-nums", children: reading === null ? "—" : `${percent}%` })] }), _jsx("div", { className: "h-1.5 overflow-hidden rounded bg-white/10", children: _jsx("div", { className: "h-full bg-emerald-400 transition-[width]", style: { width: `${percent}%` } }) }), _jsx("div", { className: "mt-1 text-[10px] text-emerald-300/70", children: reading === null ? "no signal" : String(reading.value) })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type RevealHit } from "@jgengine/core/sensor/revealQuery";
|
|
2
|
+
import type { SceneEntity } from "@jgengine/core/scene/entityStore";
|
|
3
|
+
export interface RevealVisionOptions {
|
|
4
|
+
originEntityId?: string;
|
|
5
|
+
radius: number;
|
|
6
|
+
tags: readonly string[];
|
|
7
|
+
resolveTags: (entity: SceneEntity) => readonly string[];
|
|
8
|
+
}
|
|
9
|
+
/** Occlusion-ignoring tagged-entity radius query (#115), bound to the live scene. */
|
|
10
|
+
export declare function useRevealHits(options: RevealVisionOptions): readonly RevealHit[];
|
|
11
|
+
export interface RevealHighlightsProps extends RevealVisionOptions {
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
color?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Screen-space reveal effect (#115) — highlights tagged entities through
|
|
17
|
+
* occluders (Dark Sight / detective-vision / wallhack style). Renders with
|
|
18
|
+
* `depthTest: false` so the highlight draws over any wall standing between
|
|
19
|
+
* the origin and the revealed entity, rather than the usual depth-sorted scene.
|
|
20
|
+
*/
|
|
21
|
+
export declare function RevealHighlights(props: RevealHighlightsProps): import("react").JSX.Element | null;
|
|
22
|
+
export interface RevealScreenTintProps {
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
color?: string;
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
/** Full-screen desaturating tint that reads as "vision mode is on" (Dark Sight / thermal / detective vision). */
|
|
28
|
+
export declare function RevealScreenTint({ enabled, color, className }: RevealScreenTintProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { DoubleSide } from "three";
|
|
4
|
+
import { createRevealQuery } from "@jgengine/core/sensor/revealQuery";
|
|
5
|
+
import { usePlayer, useSceneEntities } from "@jgengine/react/hooks";
|
|
6
|
+
/** Occlusion-ignoring tagged-entity radius query (#115), bound to the live scene. */
|
|
7
|
+
export function useRevealHits(options) {
|
|
8
|
+
const entities = useSceneEntities();
|
|
9
|
+
const player = usePlayer();
|
|
10
|
+
const originId = options.originEntityId ?? player.userId;
|
|
11
|
+
const tagsKey = options.tags.join("|");
|
|
12
|
+
return useMemo(() => {
|
|
13
|
+
const byId = new Map(entities.map((entity) => [entity.id, entity]));
|
|
14
|
+
const query = createRevealQuery({
|
|
15
|
+
resolvePosition: (id) => byId.get(id)?.position,
|
|
16
|
+
resolveTags: (id) => {
|
|
17
|
+
const entity = byId.get(id);
|
|
18
|
+
return entity === undefined ? [] : options.resolveTags(entity);
|
|
19
|
+
},
|
|
20
|
+
candidates: () => entities.map((entity) => entity.id),
|
|
21
|
+
});
|
|
22
|
+
return query.inRadius(originId, options.radius, options.tags);
|
|
23
|
+
}, [entities, originId, options.radius, tagsKey, options.tags, options.resolveTags]);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Screen-space reveal effect (#115) — highlights tagged entities through
|
|
27
|
+
* occluders (Dark Sight / detective-vision / wallhack style). Renders with
|
|
28
|
+
* `depthTest: false` so the highlight draws over any wall standing between
|
|
29
|
+
* the origin and the revealed entity, rather than the usual depth-sorted scene.
|
|
30
|
+
*/
|
|
31
|
+
export function RevealHighlights(props) {
|
|
32
|
+
const { enabled, color = "#7dd3fc" } = props;
|
|
33
|
+
const hits = useRevealHits(props);
|
|
34
|
+
const entities = useSceneEntities();
|
|
35
|
+
if (!enabled)
|
|
36
|
+
return null;
|
|
37
|
+
return (_jsx(_Fragment, { children: hits.map((hit) => {
|
|
38
|
+
const entity = entities.find((candidate) => candidate.id === hit.instanceId);
|
|
39
|
+
if (entity === undefined)
|
|
40
|
+
return null;
|
|
41
|
+
const pulse = Math.max(0.25, 1 - hit.distance / Math.max(props.radius, 1));
|
|
42
|
+
return (_jsxs("group", { position: [entity.position[0], entity.position[1], entity.position[2]], children: [_jsxs("mesh", { "position-y": 0.95, renderOrder: 999, children: [_jsx("capsuleGeometry", { args: [0.42, 1.2, 6, 14] }), _jsx("meshBasicMaterial", { color: color, transparent: true, opacity: 0.55 + pulse * 0.35, depthTest: false })] }), _jsxs("mesh", { "position-y": 2.6, renderOrder: 999, children: [_jsx("coneGeometry", { args: [0.14, 0.3, 12] }), _jsx("meshBasicMaterial", { color: color, transparent: true, opacity: 0.85, depthTest: false })] }), _jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.04, renderOrder: 999, children: [_jsx("ringGeometry", { args: [0.55, 0.85, 32] }), _jsx("meshBasicMaterial", { color: color, transparent: true, opacity: 0.9, depthTest: false, side: DoubleSide })] })] }, hit.instanceId));
|
|
43
|
+
}) }));
|
|
44
|
+
}
|
|
45
|
+
/** Full-screen desaturating tint that reads as "vision mode is on" (Dark Sight / thermal / detective vision). */
|
|
46
|
+
export function RevealScreenTint({ enabled, color = "rgba(56, 189, 248, 0.16)", className }) {
|
|
47
|
+
if (!enabled)
|
|
48
|
+
return null;
|
|
49
|
+
return (_jsx("div", { className: className ?? "pointer-events-none absolute inset-0 z-10 mix-blend-screen", style: {
|
|
50
|
+
background: `radial-gradient(circle at 50% 50%, transparent 25%, ${color} 100%)`,
|
|
51
|
+
filter: "saturate(0.55) contrast(1.15) brightness(0.92)",
|
|
52
|
+
} }));
|
|
53
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { type ResolvedOceanConfig } from "./OceanConfig.js";
|
|
3
|
+
export interface OceanMaterialUniforms {
|
|
4
|
+
uTime: {
|
|
5
|
+
value: number;
|
|
6
|
+
};
|
|
7
|
+
uWaveDirections: {
|
|
8
|
+
value: THREE.Vector2[];
|
|
9
|
+
};
|
|
10
|
+
uWaveParams: {
|
|
11
|
+
value: THREE.Vector4[];
|
|
12
|
+
};
|
|
13
|
+
uChoppiness: {
|
|
14
|
+
value: number;
|
|
15
|
+
};
|
|
16
|
+
uShallowColor: {
|
|
17
|
+
value: THREE.Color;
|
|
18
|
+
};
|
|
19
|
+
uDeepColor: {
|
|
20
|
+
value: THREE.Color;
|
|
21
|
+
};
|
|
22
|
+
uCrestColor: {
|
|
23
|
+
value: THREE.Color;
|
|
24
|
+
};
|
|
25
|
+
uFoamColor: {
|
|
26
|
+
value: THREE.Color;
|
|
27
|
+
};
|
|
28
|
+
uOpacity: {
|
|
29
|
+
value: number;
|
|
30
|
+
};
|
|
31
|
+
uFresnelStrength: {
|
|
32
|
+
value: number;
|
|
33
|
+
};
|
|
34
|
+
uHorizonBlend: {
|
|
35
|
+
value: number;
|
|
36
|
+
};
|
|
37
|
+
uFoamThreshold: {
|
|
38
|
+
value: number;
|
|
39
|
+
};
|
|
40
|
+
uFoamSoftness: {
|
|
41
|
+
value: number;
|
|
42
|
+
};
|
|
43
|
+
uFoamIntensity: {
|
|
44
|
+
value: number;
|
|
45
|
+
};
|
|
46
|
+
uFoamCoverage: {
|
|
47
|
+
value: number;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export type OceanShaderMaterial = THREE.ShaderMaterial & {
|
|
51
|
+
uniforms: OceanMaterialUniforms;
|
|
52
|
+
};
|
|
53
|
+
export declare function createOceanMaterial(config: ResolvedOceanConfig): OceanShaderMaterial;
|
|
54
|
+
export declare function syncOceanMaterial(material: OceanShaderMaterial, config: ResolvedOceanConfig, elapsedSeconds: number): void;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { buildOceanWaveUniforms } from "./OceanConfig.js";
|
|
3
|
+
import { oceanFragmentShader, oceanVertexShader } from "./OceanShader.js";
|
|
4
|
+
export function createOceanMaterial(config) {
|
|
5
|
+
const material = new THREE.ShaderMaterial({
|
|
6
|
+
vertexShader: oceanVertexShader,
|
|
7
|
+
fragmentShader: oceanFragmentShader,
|
|
8
|
+
side: THREE.DoubleSide,
|
|
9
|
+
transparent: config.color.opacity < 1,
|
|
10
|
+
depthWrite: config.color.opacity >= 0.98,
|
|
11
|
+
uniforms: {
|
|
12
|
+
uTime: { value: 0 },
|
|
13
|
+
uWaveDirections: { value: [] },
|
|
14
|
+
uWaveParams: { value: [] },
|
|
15
|
+
uChoppiness: { value: config.choppiness },
|
|
16
|
+
uShallowColor: { value: new THREE.Color(config.color.shallow) },
|
|
17
|
+
uDeepColor: { value: new THREE.Color(config.color.deep) },
|
|
18
|
+
uCrestColor: { value: new THREE.Color(config.color.crest) },
|
|
19
|
+
uFoamColor: { value: new THREE.Color(config.color.foam) },
|
|
20
|
+
uOpacity: { value: config.color.opacity },
|
|
21
|
+
uFresnelStrength: { value: config.color.fresnelStrength },
|
|
22
|
+
uHorizonBlend: { value: config.color.horizonBlend },
|
|
23
|
+
uFoamThreshold: { value: config.foam.crestThreshold },
|
|
24
|
+
uFoamSoftness: { value: config.foam.softness },
|
|
25
|
+
uFoamIntensity: { value: config.foam.intensity },
|
|
26
|
+
uFoamCoverage: { value: config.foam.coverage },
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
syncOceanMaterial(material, config, 0);
|
|
30
|
+
return material;
|
|
31
|
+
}
|
|
32
|
+
export function syncOceanMaterial(material, config, elapsedSeconds) {
|
|
33
|
+
const waveUniforms = buildOceanWaveUniforms(config);
|
|
34
|
+
material.uniforms.uTime.value = elapsedSeconds;
|
|
35
|
+
material.uniforms.uWaveDirections.value = waveUniforms.directions;
|
|
36
|
+
material.uniforms.uWaveParams.value = waveUniforms.params;
|
|
37
|
+
material.uniforms.uChoppiness.value = config.choppiness;
|
|
38
|
+
material.uniforms.uShallowColor.value.set(config.color.shallow);
|
|
39
|
+
material.uniforms.uDeepColor.value.set(config.color.deep);
|
|
40
|
+
material.uniforms.uCrestColor.value.set(config.color.crest);
|
|
41
|
+
material.uniforms.uFoamColor.value.set(config.color.foam);
|
|
42
|
+
material.uniforms.uOpacity.value = config.color.opacity;
|
|
43
|
+
material.uniforms.uFresnelStrength.value = config.color.fresnelStrength;
|
|
44
|
+
material.uniforms.uHorizonBlend.value = config.color.horizonBlend;
|
|
45
|
+
material.uniforms.uFoamThreshold.value = config.foam.crestThreshold;
|
|
46
|
+
material.uniforms.uFoamSoftness.value = config.foam.softness;
|
|
47
|
+
material.uniforms.uFoamIntensity.value = config.foam.intensity;
|
|
48
|
+
material.uniforms.uFoamCoverage.value = config.foam.coverage;
|
|
49
|
+
material.transparent = config.color.opacity < 1;
|
|
50
|
+
material.depthWrite = config.color.opacity >= 0.98;
|
|
51
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const oceanVertexShader = "\nuniform float uTime;\nuniform vec2 uWaveDirections[6];\nuniform vec4 uWaveParams[6];\nuniform float uChoppiness;\nuniform float uFoamThreshold;\nuniform float uFoamSoftness;\nuniform float uFoamCoverage;\n\nvarying vec3 vWorldPosition;\nvarying vec3 vNormal;\nvarying float vCrest;\nvarying float vWaveHeight;\n\nvoid main() {\n vec3 displaced = position;\n vec3 tangent = vec3(1.0, 0.0, 0.0);\n vec3 bitangent = vec3(0.0, 0.0, 1.0);\n float crest = 0.0;\n\n for (int i = 0; i < 6; i++) {\n vec2 direction = normalize(uWaveDirections[i]);\n vec4 wave = uWaveParams[i];\n float k = wave.x;\n float amplitude = wave.y;\n float steepness = wave.z;\n float omega = wave.w;\n float phase = k * dot(direction, position.xz) - omega * uTime;\n float sine = sin(phase);\n float cosine = cos(phase);\n float horizontal = steepness * amplitude * uChoppiness;\n displaced.x += horizontal * direction.x * cosine;\n displaced.z += horizontal * direction.y * cosine;\n displaced.y += amplitude * sine;\n float common = horizontal * k * sine;\n tangent += vec3(-common * direction.x * direction.x, amplitude * k * direction.x * cosine, -common * direction.x * direction.y);\n bitangent += vec3(-common * direction.x * direction.y, amplitude * k * direction.y * cosine, -common * direction.y * direction.y);\n crest = max(crest, smoothstep(uFoamThreshold, uFoamThreshold + uFoamSoftness, sine * steepness + uFoamCoverage * 0.35));\n }\n\n vNormal = normalize(cross(bitangent, tangent));\n vCrest = crest;\n vWaveHeight = displaced.y;\n vec4 worldPosition = modelMatrix * vec4(displaced, 1.0);\n vWorldPosition = worldPosition.xyz;\n gl_Position = projectionMatrix * viewMatrix * worldPosition;\n}\n";
|
|
2
|
+
export declare const oceanFragmentShader = "\nuniform vec3 uShallowColor;\nuniform vec3 uDeepColor;\nuniform vec3 uCrestColor;\nuniform vec3 uFoamColor;\nuniform float uOpacity;\nuniform float uFresnelStrength;\nuniform float uHorizonBlend;\nuniform float uFoamIntensity;\n\nvarying vec3 vWorldPosition;\nvarying vec3 vNormal;\nvarying float vCrest;\nvarying float vWaveHeight;\n\nvoid main() {\n vec3 normal = normalize(vNormal);\n vec3 viewDirection = normalize(cameraPosition - vWorldPosition);\n float fresnel = pow(1.0 - max(dot(normal, viewDirection), 0.0), 4.5);\n float depthMix = smoothstep(-1.8, 1.6, vWaveHeight);\n vec3 waterColor = mix(uDeepColor, uShallowColor, depthMix);\n waterColor = mix(waterColor, uCrestColor, smoothstep(0.15, 1.2, vWaveHeight) * 0.28);\n waterColor = mix(waterColor, vec3(0.86, 0.95, 1.0), fresnel * uFresnelStrength);\n float horizon = smoothstep(0.0, 1.0, 1.0 - abs(normal.y));\n waterColor = mix(waterColor, uShallowColor, horizon * uHorizonBlend);\n float foam = clamp(vCrest * uFoamIntensity, 0.0, 1.0);\n vec3 color = mix(waterColor, uFoamColor, foam);\n gl_FragColor = vec4(color, uOpacity);\n}\n";
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export const oceanVertexShader = `
|
|
2
|
+
uniform float uTime;
|
|
3
|
+
uniform vec2 uWaveDirections[6];
|
|
4
|
+
uniform vec4 uWaveParams[6];
|
|
5
|
+
uniform float uChoppiness;
|
|
6
|
+
uniform float uFoamThreshold;
|
|
7
|
+
uniform float uFoamSoftness;
|
|
8
|
+
uniform float uFoamCoverage;
|
|
9
|
+
|
|
10
|
+
varying vec3 vWorldPosition;
|
|
11
|
+
varying vec3 vNormal;
|
|
12
|
+
varying float vCrest;
|
|
13
|
+
varying float vWaveHeight;
|
|
14
|
+
|
|
15
|
+
void main() {
|
|
16
|
+
vec3 displaced = position;
|
|
17
|
+
vec3 tangent = vec3(1.0, 0.0, 0.0);
|
|
18
|
+
vec3 bitangent = vec3(0.0, 0.0, 1.0);
|
|
19
|
+
float crest = 0.0;
|
|
20
|
+
|
|
21
|
+
for (int i = 0; i < 6; i++) {
|
|
22
|
+
vec2 direction = normalize(uWaveDirections[i]);
|
|
23
|
+
vec4 wave = uWaveParams[i];
|
|
24
|
+
float k = wave.x;
|
|
25
|
+
float amplitude = wave.y;
|
|
26
|
+
float steepness = wave.z;
|
|
27
|
+
float omega = wave.w;
|
|
28
|
+
float phase = k * dot(direction, position.xz) - omega * uTime;
|
|
29
|
+
float sine = sin(phase);
|
|
30
|
+
float cosine = cos(phase);
|
|
31
|
+
float horizontal = steepness * amplitude * uChoppiness;
|
|
32
|
+
displaced.x += horizontal * direction.x * cosine;
|
|
33
|
+
displaced.z += horizontal * direction.y * cosine;
|
|
34
|
+
displaced.y += amplitude * sine;
|
|
35
|
+
float common = horizontal * k * sine;
|
|
36
|
+
tangent += vec3(-common * direction.x * direction.x, amplitude * k * direction.x * cosine, -common * direction.x * direction.y);
|
|
37
|
+
bitangent += vec3(-common * direction.x * direction.y, amplitude * k * direction.y * cosine, -common * direction.y * direction.y);
|
|
38
|
+
crest = max(crest, smoothstep(uFoamThreshold, uFoamThreshold + uFoamSoftness, sine * steepness + uFoamCoverage * 0.35));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
vNormal = normalize(cross(bitangent, tangent));
|
|
42
|
+
vCrest = crest;
|
|
43
|
+
vWaveHeight = displaced.y;
|
|
44
|
+
vec4 worldPosition = modelMatrix * vec4(displaced, 1.0);
|
|
45
|
+
vWorldPosition = worldPosition.xyz;
|
|
46
|
+
gl_Position = projectionMatrix * viewMatrix * worldPosition;
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
49
|
+
export const oceanFragmentShader = `
|
|
50
|
+
uniform vec3 uShallowColor;
|
|
51
|
+
uniform vec3 uDeepColor;
|
|
52
|
+
uniform vec3 uCrestColor;
|
|
53
|
+
uniform vec3 uFoamColor;
|
|
54
|
+
uniform float uOpacity;
|
|
55
|
+
uniform float uFresnelStrength;
|
|
56
|
+
uniform float uHorizonBlend;
|
|
57
|
+
uniform float uFoamIntensity;
|
|
58
|
+
|
|
59
|
+
varying vec3 vWorldPosition;
|
|
60
|
+
varying vec3 vNormal;
|
|
61
|
+
varying float vCrest;
|
|
62
|
+
varying float vWaveHeight;
|
|
63
|
+
|
|
64
|
+
void main() {
|
|
65
|
+
vec3 normal = normalize(vNormal);
|
|
66
|
+
vec3 viewDirection = normalize(cameraPosition - vWorldPosition);
|
|
67
|
+
float fresnel = pow(1.0 - max(dot(normal, viewDirection), 0.0), 4.5);
|
|
68
|
+
float depthMix = smoothstep(-1.8, 1.6, vWaveHeight);
|
|
69
|
+
vec3 waterColor = mix(uDeepColor, uShallowColor, depthMix);
|
|
70
|
+
waterColor = mix(waterColor, uCrestColor, smoothstep(0.15, 1.2, vWaveHeight) * 0.28);
|
|
71
|
+
waterColor = mix(waterColor, vec3(0.86, 0.95, 1.0), fresnel * uFresnelStrength);
|
|
72
|
+
float horizon = smoothstep(0.0, 1.0, 1.0 - abs(normal.y));
|
|
73
|
+
waterColor = mix(waterColor, uShallowColor, horizon * uHorizonBlend);
|
|
74
|
+
float foam = clamp(vCrest * uFoamIntensity, 0.0, 1.0);
|
|
75
|
+
vec3 color = mix(waterColor, uFoamColor, foam);
|
|
76
|
+
gl_FragColor = vec4(color, uOpacity);
|
|
77
|
+
}
|
|
78
|
+
`;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { Ocean, type OceanProps } from "./Ocean.js";
|
|
2
|
+
export { DEFAULT_OCEAN_CONFIG, MAX_OCEAN_WAVES, OCEAN_QUALITY_PRESETS, buildOceanWaveUniforms, createOceanConfig, type OceanColorConfig, type OceanConfig, type OceanDirectionVector, type OceanFoamConfig, type OceanQualityPreset, type OceanWaveConfig, type OceanWaveDirection, type ResolvedOceanColorConfig, type ResolvedOceanConfig, type ResolvedOceanFoamConfig, type ResolvedOceanWaveConfig, } from "./OceanConfig.js";
|
|
3
|
+
export { createOceanMaterial, syncOceanMaterial, type OceanMaterialUniforms, type OceanShaderMaterial, } from "./OceanMaterial.js";
|
package/dist/water.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./water/index.js";
|
package/dist/water.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./water/index.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import type { FireGrid } from "@jgengine/core/world/weather";
|
|
3
|
+
export interface FireSpreadLayerProps {
|
|
4
|
+
grid: FireGrid;
|
|
5
|
+
cellSize: number;
|
|
6
|
+
origin?: readonly [number, number];
|
|
7
|
+
/** Sample ground height so flames sit on terrain; defaults to y=0. */
|
|
8
|
+
heightAt?: (x: number, z: number) => number;
|
|
9
|
+
/** Flame quad height. Default 1.6. */
|
|
10
|
+
flameHeight?: number;
|
|
11
|
+
burningColor?: THREE.ColorRepresentation;
|
|
12
|
+
emberColor?: THREE.ColorRepresentation;
|
|
13
|
+
}
|
|
14
|
+
export declare function FireSpreadLayer({ grid, cellSize, origin, heightAt, flameHeight, burningColor, emberColor, }: FireSpreadLayerProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
3
|
+
import { useFrame } from "@react-three/fiber";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
const HIDDEN = new THREE.Matrix4().makeScale(0, 0, 0);
|
|
6
|
+
export function FireSpreadLayer({ grid, cellSize, origin = [0, 0], heightAt, flameHeight = 1.6, burningColor = "#ff6a1a", emberColor = "#4a1206", }) {
|
|
7
|
+
const flames = useRef(null);
|
|
8
|
+
const scorch = useRef(null);
|
|
9
|
+
const capacity = grid.cols * grid.rows;
|
|
10
|
+
const flameGeometry = useMemo(() => new THREE.PlaneGeometry(cellSize * 0.9, flameHeight), [cellSize, flameHeight]);
|
|
11
|
+
const scorchGeometry = useMemo(() => {
|
|
12
|
+
const geometry = new THREE.PlaneGeometry(cellSize, cellSize);
|
|
13
|
+
geometry.rotateX(-Math.PI / 2);
|
|
14
|
+
return geometry;
|
|
15
|
+
}, [cellSize]);
|
|
16
|
+
const flameMaterial = useMemo(() => new THREE.MeshBasicMaterial({ color: burningColor, transparent: true, opacity: 0.85, depthWrite: false, side: THREE.DoubleSide }), [burningColor]);
|
|
17
|
+
const scorchMaterial = useMemo(() => new THREE.MeshBasicMaterial({ color: emberColor, transparent: true, opacity: 0.7, depthWrite: false }), [emberColor]);
|
|
18
|
+
useEffect(() => () => {
|
|
19
|
+
flameGeometry.dispose();
|
|
20
|
+
scorchGeometry.dispose();
|
|
21
|
+
flameMaterial.dispose();
|
|
22
|
+
scorchMaterial.dispose();
|
|
23
|
+
}, [flameGeometry, scorchGeometry, flameMaterial, scorchMaterial]);
|
|
24
|
+
const matrix = useMemo(() => new THREE.Matrix4(), []);
|
|
25
|
+
const position = useMemo(() => new THREE.Vector3(), []);
|
|
26
|
+
const quaternion = useMemo(() => new THREE.Quaternion(), []);
|
|
27
|
+
const scale = useMemo(() => new THREE.Vector3(1, 1, 1), []);
|
|
28
|
+
useFrame((state) => {
|
|
29
|
+
const flameMesh = flames.current;
|
|
30
|
+
const scorchMesh = scorch.current;
|
|
31
|
+
if (flameMesh === null || scorchMesh === null)
|
|
32
|
+
return;
|
|
33
|
+
const cells = grid.snapshot();
|
|
34
|
+
const flicker = 0.85 + 0.15 * Math.sin(state.clock.elapsedTime * 12);
|
|
35
|
+
let flameIndex = 0;
|
|
36
|
+
let scorchIndex = 0;
|
|
37
|
+
for (let row = 0; row < grid.rows; row += 1) {
|
|
38
|
+
for (let col = 0; col < grid.cols; col += 1) {
|
|
39
|
+
const cell = cells[row * grid.cols + col];
|
|
40
|
+
const x = origin[0] + col * cellSize;
|
|
41
|
+
const z = origin[1] + row * cellSize;
|
|
42
|
+
const groundY = heightAt?.(x, z) ?? 0;
|
|
43
|
+
if (cell.state === "burning") {
|
|
44
|
+
position.set(x, groundY + flameHeight * 0.5, z);
|
|
45
|
+
quaternion.setFromEuler(new THREE.Euler(0, state.camera.rotation.y, 0));
|
|
46
|
+
scale.set(1, flicker, 1);
|
|
47
|
+
matrix.compose(position, quaternion, scale);
|
|
48
|
+
flameMesh.setMatrixAt(flameIndex, matrix);
|
|
49
|
+
flameIndex += 1;
|
|
50
|
+
}
|
|
51
|
+
else if (cell.state === "burnt") {
|
|
52
|
+
position.set(x, groundY + 0.05, z);
|
|
53
|
+
scorchMesh.setMatrixAt(scorchIndex, matrix.compose(position, new THREE.Quaternion(), scale.set(1, 1, 1)));
|
|
54
|
+
scorchIndex += 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (let i = flameIndex; i < capacity; i += 1)
|
|
59
|
+
flameMesh.setMatrixAt(i, HIDDEN);
|
|
60
|
+
for (let i = scorchIndex; i < capacity; i += 1)
|
|
61
|
+
scorchMesh.setMatrixAt(i, HIDDEN);
|
|
62
|
+
flameMesh.count = capacity;
|
|
63
|
+
scorchMesh.count = capacity;
|
|
64
|
+
flameMesh.instanceMatrix.needsUpdate = true;
|
|
65
|
+
scorchMesh.instanceMatrix.needsUpdate = true;
|
|
66
|
+
});
|
|
67
|
+
return (_jsxs("group", { children: [_jsx("instancedMesh", { ref: scorch, args: [scorchGeometry, scorchMaterial, capacity], frustumCulled: false, renderOrder: 5 }), _jsx("instancedMesh", { ref: flames, args: [flameGeometry, flameMaterial, capacity], frustumCulled: false, renderOrder: 11 })] }));
|
|
68
|
+
}
|