@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,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
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { type TerrainSeed } from "./random.js";
|
|
3
|
+
import { type TerrainArea, type TerrainHeightSampler } from "./terrainMath.js";
|
|
4
|
+
export type GrassRange = number | readonly [min: number, max: number];
|
|
5
|
+
export interface GrassBladeGeometryOptions {
|
|
6
|
+
count?: number;
|
|
7
|
+
area?: TerrainArea;
|
|
8
|
+
seed?: TerrainSeed;
|
|
9
|
+
segments?: number;
|
|
10
|
+
height?: GrassRange;
|
|
11
|
+
width?: GrassRange;
|
|
12
|
+
bend?: GrassRange;
|
|
13
|
+
heightAt?: TerrainHeightSampler;
|
|
14
|
+
}
|
|
15
|
+
export interface ResolvedGrassBladeGeometryOptions {
|
|
16
|
+
count: number;
|
|
17
|
+
area: TerrainArea;
|
|
18
|
+
seed: TerrainSeed;
|
|
19
|
+
segments: number;
|
|
20
|
+
height: readonly [min: number, max: number];
|
|
21
|
+
width: readonly [min: number, max: number];
|
|
22
|
+
bend: readonly [min: number, max: number];
|
|
23
|
+
heightAt: TerrainHeightSampler;
|
|
24
|
+
}
|
|
25
|
+
export declare function resolveGrassRange(value: GrassRange | undefined, fallback: readonly [number, number]): readonly [number, number];
|
|
26
|
+
export declare function resolveGrassBladeGeometryOptions(options?: GrassBladeGeometryOptions): ResolvedGrassBladeGeometryOptions;
|
|
27
|
+
export declare function createGrassBladeGeometry(options?: GrassBladeGeometryOptions): THREE.InstancedBufferGeometry;
|
|
@@ -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,10 @@
|
|
|
1
|
+
export { CarvedTerrain, type CarvedTerrainProps } from "./CarvedTerrain.js";
|
|
2
|
+
export { GrassField, type GrassFieldProps } from "./GrassField.js";
|
|
3
|
+
export { ProceduralGround, type ProceduralGroundProps } from "./ProceduralGround.js";
|
|
4
|
+
export { EditableGround, type EditableGroundProps } from "./EditableGround.js";
|
|
5
|
+
export { TerraformBrushCursor, type TerraformBrushCursorProps } from "./TerraformBrushCursor.js";
|
|
6
|
+
export { createGrassBladeGeometry, resolveGrassBladeGeometryOptions, resolveGrassRange, type GrassBladeGeometryOptions, type GrassRange, type ResolvedGrassBladeGeometryOptions, } from "./grassGeometry.js";
|
|
7
|
+
export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, type GrassMaterialHandle, type GrassMaterialOptions, type GrassShaderUniforms, type GrassWindOptions, } from "./grassMaterial.js";
|
|
8
|
+
export { createSeededRandom, hashNoise2, seedToUint32, type TerrainSeed } from "./random.js";
|
|
9
|
+
export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, type FieldGroundOptions, type ProceduralTerrainConfig, type ResolvedTerrainSegments, type ResolvedTerrainSize, type TerrainArea, type TerrainHeightSampler, type TerrainVertexColorOptions, } from "./terrainMath.js";
|
|
10
|
+
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,10 @@
|
|
|
1
|
+
export { CarvedTerrain } from "./CarvedTerrain.js";
|
|
2
|
+
export { GrassField } from "./GrassField.js";
|
|
3
|
+
export { ProceduralGround } from "./ProceduralGround.js";
|
|
4
|
+
export { EditableGround } from "./EditableGround.js";
|
|
5
|
+
export { TerraformBrushCursor } from "./TerraformBrushCursor.js";
|
|
6
|
+
export { createGrassBladeGeometry, resolveGrassBladeGeometryOptions, resolveGrassRange, } from "./grassGeometry.js";
|
|
7
|
+
export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, } from "./grassMaterial.js";
|
|
8
|
+
export { createSeededRandom, hashNoise2, seedToUint32 } from "./random.js";
|
|
9
|
+
export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, } from "./terrainMath.js";
|
|
10
|
+
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,47 @@
|
|
|
1
|
+
import { type NoiseFieldConfig, type TerrainField } 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 interface FieldGroundOptions {
|
|
35
|
+
size?: TerrainArea;
|
|
36
|
+
segments?: number | readonly [x: number, z: number];
|
|
37
|
+
center?: readonly [x: number, z: number];
|
|
38
|
+
colors?: TerrainVertexColorOptions;
|
|
39
|
+
heightRange?: readonly [min: number, max: number];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Mesh any `TerrainField` — including a `CarvableField` with craters/mounds written into it — into a
|
|
43
|
+
* vertex-coloured ground geometry. `sampleHeight` drives the vertices, so runtime carves show up as
|
|
44
|
+
* real depressions the moment the field is re-sampled (bump the caller's rebuild key after a carve).
|
|
45
|
+
*/
|
|
46
|
+
export declare function createFieldGroundGeometry(field: TerrainField, options?: FieldGroundOptions): THREE.BufferGeometry;
|
|
47
|
+
export declare function createProceduralGroundGeometry(config?: ProceduralTerrainConfig, colors?: TerrainVertexColorOptions): THREE.BufferGeometry;
|
|
@@ -0,0 +1,172 @@
|
|
|
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
|
+
/**
|
|
27
|
+
* Mesh any `TerrainField` — including a `CarvableField` with craters/mounds written into it — into a
|
|
28
|
+
* vertex-coloured ground geometry. `sampleHeight` drives the vertices, so runtime carves show up as
|
|
29
|
+
* real depressions the moment the field is re-sampled (bump the caller's rebuild key after a carve).
|
|
30
|
+
*/
|
|
31
|
+
export function createFieldGroundGeometry(field, options = {}) {
|
|
32
|
+
const size = resolveTerrainSize(options.size);
|
|
33
|
+
const segments = resolveTerrainSegments(options.segments);
|
|
34
|
+
const [cx, cz] = options.center ?? [0, 0];
|
|
35
|
+
return buildGroundGeometry(size, segments, (x, z) => field.sampleHeight(x, z), {
|
|
36
|
+
colors: options.colors ?? {},
|
|
37
|
+
center: [cx, cz],
|
|
38
|
+
heightRange: options.heightRange,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function buildGroundGeometry(size, segments, sampler, opts) {
|
|
42
|
+
const colors = opts.colors;
|
|
43
|
+
const vertexCountX = segments.x + 1;
|
|
44
|
+
const vertexCountZ = segments.z + 1;
|
|
45
|
+
const positions = new Float32Array(vertexCountX * vertexCountZ * 3);
|
|
46
|
+
const uvs = new Float32Array(vertexCountX * vertexCountZ * 2);
|
|
47
|
+
const colorValues = new Float32Array(vertexCountX * vertexCountZ * 3);
|
|
48
|
+
const indices = new Uint32Array(segments.x * segments.z * 6);
|
|
49
|
+
const low = new THREE.Color(colors.low ?? "#30402c");
|
|
50
|
+
const high = new THREE.Color(colors.high ?? "#7f8b50");
|
|
51
|
+
const waterline = colors.waterline === undefined ? null : new THREE.Color(colors.waterline);
|
|
52
|
+
const minHeight = opts.heightRange?.[0] ?? -3;
|
|
53
|
+
const maxHeight = opts.heightRange?.[1] ?? 3;
|
|
54
|
+
let index = 0;
|
|
55
|
+
let uvIndex = 0;
|
|
56
|
+
let colorIndex = 0;
|
|
57
|
+
for (let zIndex = 0; zIndex < vertexCountZ; zIndex += 1) {
|
|
58
|
+
const v = zIndex / segments.z;
|
|
59
|
+
const z = opts.center[1] + (v - 0.5) * size.depth;
|
|
60
|
+
for (let xIndex = 0; xIndex < vertexCountX; xIndex += 1) {
|
|
61
|
+
const u = xIndex / segments.x;
|
|
62
|
+
const x = opts.center[0] + (u - 0.5) * size.width;
|
|
63
|
+
const y = sampler(x, z);
|
|
64
|
+
positions[index] = x;
|
|
65
|
+
positions[index + 1] = y;
|
|
66
|
+
positions[index + 2] = z;
|
|
67
|
+
uvs[uvIndex] = u;
|
|
68
|
+
uvs[uvIndex + 1] = v;
|
|
69
|
+
const blend = THREE.MathUtils.clamp((y - minHeight) / (maxHeight - minHeight), 0, 1);
|
|
70
|
+
const color = low.clone().lerp(high, blend);
|
|
71
|
+
if (waterline !== null && y <= (colors.waterlineHeight ?? 0))
|
|
72
|
+
color.lerp(waterline, 0.65);
|
|
73
|
+
colorValues[colorIndex] = color.r;
|
|
74
|
+
colorValues[colorIndex + 1] = color.g;
|
|
75
|
+
colorValues[colorIndex + 2] = color.b;
|
|
76
|
+
index += 3;
|
|
77
|
+
uvIndex += 2;
|
|
78
|
+
colorIndex += 3;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
let triangleIndex = 0;
|
|
82
|
+
for (let zIndex = 0; zIndex < segments.z; zIndex += 1) {
|
|
83
|
+
for (let xIndex = 0; xIndex < segments.x; xIndex += 1) {
|
|
84
|
+
const a = zIndex * vertexCountX + xIndex;
|
|
85
|
+
const b = a + 1;
|
|
86
|
+
const c = a + vertexCountX;
|
|
87
|
+
const d = c + 1;
|
|
88
|
+
indices[triangleIndex] = a;
|
|
89
|
+
indices[triangleIndex + 1] = c;
|
|
90
|
+
indices[triangleIndex + 2] = b;
|
|
91
|
+
indices[triangleIndex + 3] = b;
|
|
92
|
+
indices[triangleIndex + 4] = c;
|
|
93
|
+
indices[triangleIndex + 5] = d;
|
|
94
|
+
triangleIndex += 6;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const geometry = new THREE.BufferGeometry();
|
|
98
|
+
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
|
99
|
+
geometry.setAttribute("uv", new THREE.BufferAttribute(uvs, 2));
|
|
100
|
+
geometry.setAttribute("color", new THREE.BufferAttribute(colorValues, 3));
|
|
101
|
+
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
|
|
102
|
+
geometry.computeVertexNormals();
|
|
103
|
+
geometry.computeBoundingSphere();
|
|
104
|
+
return geometry;
|
|
105
|
+
}
|
|
106
|
+
export function createProceduralGroundGeometry(config = {}, colors = {}) {
|
|
107
|
+
const size = resolveTerrainSize(config.size);
|
|
108
|
+
const segments = resolveTerrainSegments(config.segments);
|
|
109
|
+
const sampler = createProceduralTerrainSampler(config);
|
|
110
|
+
const vertexCountX = segments.x + 1;
|
|
111
|
+
const vertexCountZ = segments.z + 1;
|
|
112
|
+
const positions = new Float32Array(vertexCountX * vertexCountZ * 3);
|
|
113
|
+
const uvs = new Float32Array(vertexCountX * vertexCountZ * 2);
|
|
114
|
+
const colorValues = new Float32Array(vertexCountX * vertexCountZ * 3);
|
|
115
|
+
const indices = new Uint32Array(segments.x * segments.z * 6);
|
|
116
|
+
const low = new THREE.Color(colors.low ?? "#30402c");
|
|
117
|
+
const high = new THREE.Color(colors.high ?? "#7f8b50");
|
|
118
|
+
const waterline = colors.waterline === undefined ? null : new THREE.Color(colors.waterline);
|
|
119
|
+
const minHeight = (config.baseOffset ?? 0) - (config.height ?? 1.4) * 1.2;
|
|
120
|
+
const maxHeight = (config.baseOffset ?? 0) + (config.height ?? 1.4) * 1.2;
|
|
121
|
+
let index = 0;
|
|
122
|
+
let uvIndex = 0;
|
|
123
|
+
let colorIndex = 0;
|
|
124
|
+
for (let zIndex = 0; zIndex < vertexCountZ; zIndex += 1) {
|
|
125
|
+
const v = zIndex / segments.z;
|
|
126
|
+
const z = (v - 0.5) * size.depth;
|
|
127
|
+
for (let xIndex = 0; xIndex < vertexCountX; xIndex += 1) {
|
|
128
|
+
const u = xIndex / segments.x;
|
|
129
|
+
const x = (u - 0.5) * size.width;
|
|
130
|
+
const y = sampler(x, z);
|
|
131
|
+
positions[index] = x;
|
|
132
|
+
positions[index + 1] = y;
|
|
133
|
+
positions[index + 2] = z;
|
|
134
|
+
uvs[uvIndex] = u;
|
|
135
|
+
uvs[uvIndex + 1] = v;
|
|
136
|
+
const blend = THREE.MathUtils.clamp((y - minHeight) / (maxHeight - minHeight), 0, 1);
|
|
137
|
+
const color = low.clone().lerp(high, blend);
|
|
138
|
+
if (waterline !== null && y <= (colors.waterlineHeight ?? 0))
|
|
139
|
+
color.lerp(waterline, 0.65);
|
|
140
|
+
colorValues[colorIndex] = color.r;
|
|
141
|
+
colorValues[colorIndex + 1] = color.g;
|
|
142
|
+
colorValues[colorIndex + 2] = color.b;
|
|
143
|
+
index += 3;
|
|
144
|
+
uvIndex += 2;
|
|
145
|
+
colorIndex += 3;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
let triangleIndex = 0;
|
|
149
|
+
for (let zIndex = 0; zIndex < segments.z; zIndex += 1) {
|
|
150
|
+
for (let xIndex = 0; xIndex < segments.x; xIndex += 1) {
|
|
151
|
+
const a = zIndex * vertexCountX + xIndex;
|
|
152
|
+
const b = a + 1;
|
|
153
|
+
const c = a + vertexCountX;
|
|
154
|
+
const d = c + 1;
|
|
155
|
+
indices[triangleIndex] = a;
|
|
156
|
+
indices[triangleIndex + 1] = c;
|
|
157
|
+
indices[triangleIndex + 2] = b;
|
|
158
|
+
indices[triangleIndex + 3] = b;
|
|
159
|
+
indices[triangleIndex + 4] = c;
|
|
160
|
+
indices[triangleIndex + 5] = d;
|
|
161
|
+
triangleIndex += 6;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const geometry = new THREE.BufferGeometry();
|
|
165
|
+
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
|
166
|
+
geometry.setAttribute("uv", new THREE.BufferAttribute(uvs, 2));
|
|
167
|
+
geometry.setAttribute("color", new THREE.BufferAttribute(colorValues, 3));
|
|
168
|
+
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
|
|
169
|
+
geometry.computeVertexNormals();
|
|
170
|
+
geometry.computeBoundingSphere();
|
|
171
|
+
return geometry;
|
|
172
|
+
}
|
|
@@ -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,21 @@
|
|
|
1
|
+
import { type FramingConfig, type FrustumSample } from "@jgengine/core/sensor/frustumSensor";
|
|
2
|
+
export interface FrustumSensorProbeOptions extends FramingConfig {
|
|
3
|
+
subjectIds: readonly string[];
|
|
4
|
+
subjectRadius?: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* View-frustum sensor (#117) driven by the live render camera: which held-camera
|
|
8
|
+
* subjects are in frame, how well framed, and their dwell time on-screen
|
|
9
|
+
* (Content Warning-style "is this monster filmed well" scoring). Returns the
|
|
10
|
+
* best-framed in-view subject each frame for a photo-mode HUD readout.
|
|
11
|
+
*/
|
|
12
|
+
export declare function useFrustumSensor(options: FrustumSensorProbeOptions): FrustumSample | null;
|
|
13
|
+
/**
|
|
14
|
+
* Renders inside the Canvas (needs the live camera via `useFrame`/`useThree`)
|
|
15
|
+
* but portals a real HTML readout via drei's `Html fullscreen` — a photo-mode
|
|
16
|
+
* "is this subject framed" HUD.
|
|
17
|
+
*/
|
|
18
|
+
export declare function FrustumSensorReadout(props: FrustumSensorProbeOptions & {
|
|
19
|
+
wrapperClassName?: string;
|
|
20
|
+
className?: string;
|
|
21
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Html } from "@react-three/drei";
|
|
3
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
4
|
+
import { useMemo, useRef, useState } from "react";
|
|
5
|
+
import { PerspectiveCamera, Vector3 } from "three";
|
|
6
|
+
import { createFrustumSensor, } from "@jgengine/core/sensor/frustumSensor";
|
|
7
|
+
import { useSceneEntities } from "@jgengine/react/hooks";
|
|
8
|
+
function isPerspective(camera) {
|
|
9
|
+
return camera.isPerspectiveCamera === true;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* View-frustum sensor (#117) driven by the live render camera: which held-camera
|
|
13
|
+
* subjects are in frame, how well framed, and their dwell time on-screen
|
|
14
|
+
* (Content Warning-style "is this monster filmed well" scoring). Returns the
|
|
15
|
+
* best-framed in-view subject each frame for a photo-mode HUD readout.
|
|
16
|
+
*/
|
|
17
|
+
export function useFrustumSensor(options) {
|
|
18
|
+
const camera = useThree((state) => state.camera);
|
|
19
|
+
const size = useThree((state) => state.size);
|
|
20
|
+
const entities = useSceneEntities();
|
|
21
|
+
const sensor = useMemo(() => createFrustumSensor(options), []);
|
|
22
|
+
const [best, setBest] = useState(null);
|
|
23
|
+
const forward = useRef(new Vector3());
|
|
24
|
+
useFrame((_, dt) => {
|
|
25
|
+
const targets = [];
|
|
26
|
+
for (const id of options.subjectIds) {
|
|
27
|
+
const entity = entities.find((candidate) => candidate.id === id);
|
|
28
|
+
if (entity === undefined)
|
|
29
|
+
continue;
|
|
30
|
+
targets.push({ id, position: entity.position, radius: options.subjectRadius });
|
|
31
|
+
}
|
|
32
|
+
if (targets.length === 0) {
|
|
33
|
+
setBest(null);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
camera.getWorldDirection(forward.current);
|
|
37
|
+
const samples = sensor.tick({
|
|
38
|
+
position: [camera.position.x, camera.position.y, camera.position.z],
|
|
39
|
+
lookAt: [
|
|
40
|
+
camera.position.x + forward.current.x,
|
|
41
|
+
camera.position.y + forward.current.y,
|
|
42
|
+
camera.position.z + forward.current.z,
|
|
43
|
+
],
|
|
44
|
+
fovDeg: isPerspective(camera) ? camera.fov : 55,
|
|
45
|
+
aspect: size.height === 0 ? 16 / 9 : size.width / size.height,
|
|
46
|
+
}, targets, dt);
|
|
47
|
+
let leader = null;
|
|
48
|
+
for (const sample of samples) {
|
|
49
|
+
if (!sample.inView)
|
|
50
|
+
continue;
|
|
51
|
+
if (leader === null || sample.framing > leader.framing)
|
|
52
|
+
leader = sample;
|
|
53
|
+
}
|
|
54
|
+
setBest(leader);
|
|
55
|
+
});
|
|
56
|
+
return best;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Renders inside the Canvas (needs the live camera via `useFrame`/`useThree`)
|
|
60
|
+
* but portals a real HTML readout via drei's `Html fullscreen` — a photo-mode
|
|
61
|
+
* "is this subject framed" HUD.
|
|
62
|
+
*/
|
|
63
|
+
export function FrustumSensorReadout(props) {
|
|
64
|
+
const sample = useFrustumSensor(props);
|
|
65
|
+
return (_jsx(Html, { fullscreen: true, children: _jsx("div", { className: props.wrapperClassName ?? "pointer-events-none absolute bottom-24 right-4", children: sample === null ? (_jsx("div", { className: props.className ?? "rounded border border-white/15 bg-black/60 px-3 py-1.5 text-xs text-white/50", children: "No subject framed" })) : (_jsxs("div", { className: props.className ??
|
|
66
|
+
"rounded border border-amber-300/40 bg-black/70 px-3 py-1.5 text-xs text-amber-200 shadow-lg", children: [_jsx("span", { className: "font-semibold uppercase tracking-wide", children: sample.id }), _jsxs("span", { className: "ml-2 tabular-nums", children: ["framing ", Math.round(sample.framing * 100), "%"] }), _jsxs("span", { className: "ml-2 tabular-nums", children: ["dwell ", sample.dwellSeconds.toFixed(1), "s"] })] })) }) }));
|
|
67
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { probeHiddenState, type HiddenStateSource, type SensorProbeOptions } from "@jgengine/core/sensor/hiddenStateProbe";
|
|
2
|
+
import type { EntityPosition } from "@jgengine/core/scene/entityStore";
|
|
3
|
+
/** Reads a hidden zone/entity state variable in range (EMF / thermometer / geiger style sensor verb, #116). */
|
|
4
|
+
export declare function useHiddenStateProbe(origin: EntityPosition, sources: readonly HiddenStateSource[], options: SensorProbeOptions): import("@jgengine/core/sensor/hiddenStateProbe").SensorReading | null;
|
|
5
|
+
export interface SensorReadoutMeterProps {
|
|
6
|
+
label: string;
|
|
7
|
+
reading: ReturnType<typeof probeHiddenState>;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
/** A handheld-sensor readout: needle strength bar + the raw reading, or a "no signal" idle state. */
|
|
11
|
+
export declare function SensorReadoutMeter({ label, reading, className }: SensorReadoutMeterProps): import("react").JSX.Element;
|