mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-03-21 21:31:14 +00:00
use instancing to improve performance
This commit is contained in:
parent
78e791f763
commit
cd2819d28a
33 changed files with 482 additions and 319 deletions
|
|
@ -17,7 +17,7 @@ import {
|
|||
BufferGeometry,
|
||||
Group,
|
||||
} from "three";
|
||||
import type { AnimationAction } from "three";
|
||||
import type { AnimationAction, Material } from "three";
|
||||
import * as SkeletonUtils from "three/examples/jsm/utils/SkeletonUtils.js";
|
||||
import { setupTexture } from "../textureUtils";
|
||||
import { useAnisotropy } from "./useAnisotropy";
|
||||
|
|
@ -84,19 +84,27 @@ type MaterialResult =
|
|||
| SingleMaterial
|
||||
| [MeshLambertMaterial, MeshLambertMaterial];
|
||||
|
||||
// Stable onBeforeCompile callbacks — using shared function references lets
|
||||
// Three.js's program cache match by identity rather than toString().
|
||||
const lambertBeforeCompile: Material["onBeforeCompile"] = (shader) => {
|
||||
injectCustomFog(shader, globalFogUniforms);
|
||||
injectShapeLighting(shader);
|
||||
};
|
||||
|
||||
const basicBeforeCompile: Material["onBeforeCompile"] = (shader) => {
|
||||
injectCustomFog(shader, globalFogUniforms);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to apply volumetric fog and lighting multipliers to a material
|
||||
* Helper to apply volumetric fog and lighting multipliers to a material.
|
||||
*/
|
||||
export function applyShapeShaderModifications(
|
||||
mat: MeshBasicMaterial | MeshLambertMaterial,
|
||||
): void {
|
||||
mat.onBeforeCompile = (shader) => {
|
||||
injectCustomFog(shader, globalFogUniforms);
|
||||
// Only inject lighting for Lambert materials (Basic materials are unlit)
|
||||
if (mat instanceof MeshLambertMaterial) {
|
||||
injectShapeLighting(shader);
|
||||
}
|
||||
};
|
||||
mat.onBeforeCompile =
|
||||
mat instanceof MeshLambertMaterial
|
||||
? lambertBeforeCompile
|
||||
: basicBeforeCompile;
|
||||
}
|
||||
|
||||
export function createMaterialFromFlags(
|
||||
|
|
|
|||
|
|
@ -57,22 +57,23 @@ export function ScoreScreen({ onClose }: { onClose: () => void }) {
|
|||
const dialogRef = useRef<HTMLDivElement>(null);
|
||||
const dataSource = useDataSource();
|
||||
const isLive = dataSource === "live";
|
||||
const connectedClientId = useEngineSelector(
|
||||
(state) => state.playback.streamSnapshot?.connectedClientId,
|
||||
);
|
||||
|
||||
const teamScores = useEngineSelector(
|
||||
(state) => state.playback.streamSnapshot?.teamScores,
|
||||
);
|
||||
const playerRoster = useEngineSelector(
|
||||
(state) => state.playback.streamSnapshot?.playerRoster,
|
||||
);
|
||||
const playerSensorGroup = useEngineSelector(
|
||||
(state) => state.playback.streamSnapshot?.playerSensorGroup,
|
||||
);
|
||||
const matchClockMs = useEngineSelector(
|
||||
(state) => state.playback.streamSnapshot?.matchClockMs,
|
||||
);
|
||||
const { connectedClientId, teamScores, playerRoster, matchClockMs } =
|
||||
useEngineSelector(
|
||||
(state) => {
|
||||
const snap = state.playback.streamSnapshot;
|
||||
return {
|
||||
connectedClientId: snap?.connectedClientId,
|
||||
teamScores: snap?.teamScores,
|
||||
playerRoster: snap?.playerRoster,
|
||||
matchClockMs: snap?.matchClockMs,
|
||||
};
|
||||
},
|
||||
(a, b) =>
|
||||
a.connectedClientId === b.connectedClientId &&
|
||||
a.teamScores === b.teamScores &&
|
||||
a.playerRoster === b.playerRoster &&
|
||||
a.matchClockMs === b.matchClockMs,
|
||||
);
|
||||
|
||||
// Focus and exit pointer lock on open
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { memo, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { memo, useEffect, useMemo, useRef } from "react";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
|
|
@ -7,7 +7,9 @@ import {
|
|||
DataTexture,
|
||||
Float32BufferAttribute,
|
||||
FloatType,
|
||||
InstancedMesh as ThreeInstancedMesh,
|
||||
LinearFilter,
|
||||
Matrix4,
|
||||
NearestFilter,
|
||||
NoColorSpace,
|
||||
ClampToEdgeWrapping,
|
||||
|
|
@ -25,7 +27,7 @@ import { useSceneSky, useSceneSun } from "../state/gameEntityStore";
|
|||
import { loadTerrain } from "../loaders";
|
||||
import { uint16ToFloat32 } from "../arrayUtils";
|
||||
import { setupMask } from "../textureUtils";
|
||||
import { TerrainTile } from "./TerrainTile";
|
||||
import { TerrainTile, TerrainMaterial } from "./TerrainTile";
|
||||
import {
|
||||
createTerrainHeightSampler,
|
||||
setTerrainHeightSampler,
|
||||
|
|
@ -447,10 +449,6 @@ function useVisibleDistance(): number {
|
|||
? sky.visibleDistance
|
||||
: DEFAULT_VISIBLE_DISTANCE;
|
||||
}
|
||||
interface TileAssignment {
|
||||
tileX: number;
|
||||
tileZ: number;
|
||||
}
|
||||
/**
|
||||
* Create a visibility mask texture from emptySquares data.
|
||||
*/
|
||||
|
|
@ -574,27 +572,33 @@ export const TerrainBlock = memo(function TerrainBlock({
|
|||
const gridSize = 2 * extent + 1;
|
||||
return gridSize * gridSize - 1; // -1 because primary tile is separate
|
||||
}, [visibleDistance, blockSize]);
|
||||
// Create stable pool indices for React keys
|
||||
const poolIndices = useMemo(
|
||||
() => Array.from({ length: poolSize }, (_, i) => i),
|
||||
[poolSize],
|
||||
);
|
||||
// Track which tile coordinate each pool slot is assigned to
|
||||
const [tileAssignments, setTileAssignments] = useState<
|
||||
(TileAssignment | null)[]
|
||||
>(() => Array(poolSize).fill(null));
|
||||
// Track previous tile bounds to avoid unnecessary state updates
|
||||
const prevBoundsRef = useRef({ xStart: 0, xEnd: 0, zStart: 0, zEnd: 0 });
|
||||
// InstancedMesh ref and reusable matrix for pooled terrain tiles.
|
||||
const pooledMeshRef = useRef<ThreeInstancedMesh>(null);
|
||||
const _tileMatrix = useMemo(() => new Matrix4(), []);
|
||||
// Track previous tile bounds to avoid unnecessary instance matrix updates.
|
||||
const prevBoundsRef = useRef({
|
||||
xStart: Infinity,
|
||||
xEnd: -Infinity,
|
||||
zStart: Infinity,
|
||||
zEnd: -Infinity,
|
||||
});
|
||||
// Track which mesh instance we last updated — when r3f recreates the
|
||||
// InstancedMesh (e.g. poolSize changes), we must force a full update
|
||||
// even if the camera bounds haven't changed.
|
||||
const lastMeshRef = useRef<ThreeInstancedMesh | null>(null);
|
||||
useFrame(() => {
|
||||
const mesh = pooledMeshRef.current;
|
||||
if (!mesh) return;
|
||||
const relativeCamX = camera.position.x - basePosition.x;
|
||||
const relativeCamZ = camera.position.z - basePosition.z;
|
||||
const xStart = Math.floor((relativeCamX - visibleDistance) / blockSize);
|
||||
const xEnd = Math.ceil((relativeCamX + visibleDistance) / blockSize);
|
||||
const zStart = Math.floor((relativeCamZ - visibleDistance) / blockSize);
|
||||
const zEnd = Math.ceil((relativeCamZ + visibleDistance) / blockSize);
|
||||
// Early exit if bounds haven't changed
|
||||
// Early exit if bounds haven't changed AND we're still on the same mesh.
|
||||
const prev = prevBoundsRef.current;
|
||||
if (
|
||||
mesh === lastMeshRef.current &&
|
||||
xStart === prev.xStart &&
|
||||
xEnd === prev.xEnd &&
|
||||
zStart === prev.zStart &&
|
||||
|
|
@ -602,22 +606,27 @@ export const TerrainBlock = memo(function TerrainBlock({
|
|||
) {
|
||||
return;
|
||||
}
|
||||
lastMeshRef.current = mesh;
|
||||
prev.xStart = xStart;
|
||||
prev.xEnd = xEnd;
|
||||
prev.zStart = zStart;
|
||||
prev.zEnd = zEnd;
|
||||
// Build new assignments array
|
||||
const newAssignments: (TileAssignment | null)[] = [];
|
||||
const geometryOffset = blockSize / 2;
|
||||
let count = 0;
|
||||
for (let x = xStart; x < xEnd; x++) {
|
||||
for (let z = zStart; z < zEnd; z++) {
|
||||
if (x === 0 && z === 0) continue;
|
||||
newAssignments.push({ tileX: x, tileZ: z });
|
||||
_tileMatrix.makeTranslation(
|
||||
basePosition.x + x * blockSize + geometryOffset,
|
||||
0,
|
||||
basePosition.z + z * blockSize + geometryOffset,
|
||||
);
|
||||
mesh.setMatrixAt(count, _tileMatrix);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
while (newAssignments.length < poolSize) {
|
||||
newAssignments.push(null);
|
||||
}
|
||||
setTileAssignments(newAssignments);
|
||||
mesh.count = count;
|
||||
mesh.instanceMatrix.needsUpdate = true;
|
||||
});
|
||||
if (
|
||||
!terrain ||
|
||||
|
|
@ -650,27 +659,25 @@ export const TerrainBlock = memo(function TerrainBlock({
|
|||
detailTextureName={detailTexture}
|
||||
lightmap={terrainLightmap}
|
||||
/>
|
||||
{/* Pooled tiles - stable keys, always mounted */}
|
||||
{poolIndices.map((poolIndex) => {
|
||||
const assignment = tileAssignments[poolIndex];
|
||||
return (
|
||||
<TerrainTile
|
||||
key={poolIndex}
|
||||
tileX={assignment?.tileX ?? 0}
|
||||
tileZ={assignment?.tileZ ?? 0}
|
||||
blockSize={blockSize}
|
||||
basePosition={basePosition}
|
||||
textureNames={terrain.textureNames}
|
||||
geometry={sharedGeometry}
|
||||
displacementMap={sharedDisplacementMap}
|
||||
visibilityMask={pooledVisibilityMask}
|
||||
alphaTextures={sharedAlphaTextures}
|
||||
detailTextureName={detailTexture}
|
||||
lightmap={terrainLightmap}
|
||||
visible={assignment !== null}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{/* Pooled tiles — single InstancedMesh, matrices updated in useFrame.
|
||||
All pooled tiles share the same geometry, material, and visibility mask.
|
||||
Only position differs (set via instance matrices). */}
|
||||
<instancedMesh
|
||||
ref={pooledMeshRef}
|
||||
args={[sharedGeometry, undefined, poolSize]}
|
||||
castShadow
|
||||
receiveShadow
|
||||
frustumCulled={false}
|
||||
>
|
||||
<TerrainMaterial
|
||||
displacementMap={sharedDisplacementMap}
|
||||
visibilityMask={pooledVisibilityMask}
|
||||
textureNames={terrain.textureNames}
|
||||
alphaTextures={sharedAlphaTextures}
|
||||
detailTextureName={detailTexture}
|
||||
lightmap={terrainLightmap}
|
||||
/>
|
||||
</instancedMesh>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ const BlendedTerrainTextures = memo(function BlendedTerrainTextures({
|
|||
);
|
||||
});
|
||||
|
||||
const TerrainMaterial = memo(function TerrainMaterial({
|
||||
export const TerrainMaterial = memo(function TerrainMaterial({
|
||||
displacementMap,
|
||||
visibilityMask,
|
||||
textureNames,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import { memo, Suspense, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Box, useTexture } from "@react-three/drei";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { DoubleSide, NoColorSpace, PlaneGeometry, RepeatWrapping } from "three";
|
||||
import {
|
||||
DoubleSide,
|
||||
InstancedMesh as ThreeInstancedMesh,
|
||||
Matrix4,
|
||||
NoColorSpace,
|
||||
PlaneGeometry,
|
||||
RepeatWrapping,
|
||||
} from "three";
|
||||
import { textureToUrl } from "../loaders";
|
||||
import {
|
||||
torqueToThree,
|
||||
|
|
@ -191,7 +198,10 @@ export const WaterBlock = memo(function WaterBlock({
|
|||
|
||||
// Only update state if reps actually changed (avoid unnecessary re-renders)
|
||||
setReps((prevReps) => {
|
||||
if (JSON.stringify(prevReps) === JSON.stringify(newReps)) {
|
||||
if (
|
||||
prevReps.length === newReps.length &&
|
||||
prevReps.every((r, i) => r[0] === newReps[i][0] && r[1] === newReps[i][1])
|
||||
) {
|
||||
return prevReps;
|
||||
}
|
||||
return newReps;
|
||||
|
|
@ -335,8 +345,14 @@ const WaterReps = memo(function WaterReps({
|
|||
});
|
||||
}, [opacity, waveMagnitude, envMapIntensity, baseTexture, envTexture]);
|
||||
|
||||
// Single animation loop for the shared material
|
||||
// Single animation loop for the shared material + instance matrix updates.
|
||||
const elapsedRef = useRef(0);
|
||||
const meshRef = useRef<ThreeInstancedMesh>(null);
|
||||
const matrixRef = useRef(new Matrix4());
|
||||
// Track which mesh + reps we last populated — forces a full update when
|
||||
// r3f recreates the InstancedMesh or when reps change (camera moves).
|
||||
const lastMeshRef = useRef<ThreeInstancedMesh | null>(null);
|
||||
const lastRepsRef = useRef<Array<[number, number]> | null>(null);
|
||||
|
||||
useFrame((_, delta) => {
|
||||
if (!animationEnabled) {
|
||||
|
|
@ -346,6 +362,23 @@ const WaterReps = memo(function WaterReps({
|
|||
elapsedRef.current += delta;
|
||||
material.uniforms.uTime.value = elapsedRef.current;
|
||||
}
|
||||
|
||||
// Update instance matrices when reps change or mesh is recreated.
|
||||
const mesh = meshRef.current;
|
||||
if (!mesh) return;
|
||||
if (mesh === lastMeshRef.current && reps === lastRepsRef.current) return;
|
||||
lastMeshRef.current = mesh;
|
||||
lastRepsRef.current = reps;
|
||||
const mat = matrixRef.current;
|
||||
for (let i = 0; i < reps.length; i++) {
|
||||
const [repX, repZ] = reps[i];
|
||||
const worldX = basePosition[0] + repX * REP_SIZE - TERRAIN_OFFSET;
|
||||
const worldZ = basePosition[2] + repZ * REP_SIZE - TERRAIN_OFFSET;
|
||||
mat.makeTranslation(worldX, basePosition[1], worldZ);
|
||||
mesh.setMatrixAt(i, mat);
|
||||
}
|
||||
mesh.count = reps.length;
|
||||
mesh.instanceMatrix.needsUpdate = true;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -355,21 +388,10 @@ const WaterReps = memo(function WaterReps({
|
|||
}, [material]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{reps.map(([repX, repZ]) => {
|
||||
// Convert from terrain space to world space by subtracting TERRAIN_OFFSET
|
||||
// Matches Torque's L2Wm transform: L2Wv = (-1024, -1024, 0)
|
||||
const worldX = basePosition[0] + repX * REP_SIZE - TERRAIN_OFFSET;
|
||||
const worldZ = basePosition[2] + repZ * REP_SIZE - TERRAIN_OFFSET;
|
||||
return (
|
||||
<mesh
|
||||
key={`${repX},${repZ}`}
|
||||
geometry={surfaceGeometry}
|
||||
material={material}
|
||||
position={[worldX, basePosition[1], worldZ]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
<instancedMesh
|
||||
ref={meshRef}
|
||||
args={[surfaceGeometry, material, 9]}
|
||||
frustumCulled={false}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -189,7 +189,11 @@ export const fogVertexShader = `
|
|||
|
||||
export const fogVertexShaderWorldPos = `
|
||||
#ifdef USE_FOG
|
||||
vFogWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
|
||||
vec4 _fogPos = vec4(position, 1.0);
|
||||
#ifdef USE_INSTANCING
|
||||
_fogPos = instanceMatrix * _fogPos;
|
||||
#endif
|
||||
vFogWorldPosition = (modelMatrix * _fogPos).xyz;
|
||||
#endif
|
||||
`;
|
||||
|
||||
|
|
@ -243,7 +247,11 @@ export function installCustomFogShader(): void {
|
|||
// This ensures fog doesn't change when rotating the camera
|
||||
vFogDepth = length(mvPosition.xyz);
|
||||
#ifdef USE_FOG_WORLD_POSITION
|
||||
vFogWorldPosition = (modelMatrix * vec4(transformed, 1.0)).xyz;
|
||||
vec4 _fogPos2 = vec4(transformed, 1.0);
|
||||
#ifdef USE_INSTANCING
|
||||
_fogPos2 = instanceMatrix * _fogPos2;
|
||||
#endif
|
||||
vFogWorldPosition = (modelMatrix * _fogPos2).xyz;
|
||||
#endif
|
||||
#endif
|
||||
`;
|
||||
|
|
@ -310,7 +318,11 @@ export function injectCustomFog(
|
|||
"#include <fog_vertex>",
|
||||
`#include <fog_vertex>
|
||||
#ifdef USE_FOG
|
||||
vFogWorldPosition = (modelMatrix * vec4(transformed, 1.0)).xyz;
|
||||
vec4 _fogPos3 = vec4(transformed, 1.0);
|
||||
#ifdef USE_INSTANCING
|
||||
_fogPos3 = instanceMatrix * _fogPos3;
|
||||
#endif
|
||||
vFogWorldPosition = (modelMatrix * _fogPos3).xyz;
|
||||
#endif`,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useMemo } from "react";
|
||||
import { createStore } from "zustand/vanilla";
|
||||
import { subscribeWithSelector } from "zustand/middleware";
|
||||
import { useStoreWithEqualityFn } from "zustand/traditional";
|
||||
|
|
@ -375,11 +376,11 @@ export function useRuntimeObjectById(
|
|||
id == null ? -1 : (state.runtime.objectVersionById[id] ?? -1),
|
||||
);
|
||||
|
||||
if (id == null || !runtime || version === -1) {
|
||||
return undefined;
|
||||
}
|
||||
const object = runtime.state.objectsById.get(id);
|
||||
return object ? { ...object } : undefined;
|
||||
return useMemo(() => {
|
||||
if (id == null || !runtime || version === -1) return undefined;
|
||||
const object = runtime.state.objectsById.get(id);
|
||||
return object ? { ...object } : undefined;
|
||||
}, [id, runtime, version]);
|
||||
}
|
||||
|
||||
export function useRuntimeObjectField<T = any>(
|
||||
|
|
@ -431,11 +432,13 @@ export function useRuntimeObjectByName(
|
|||
objectId == null ? -1 : (state.runtime.objectVersionById[objectId] ?? -1),
|
||||
);
|
||||
|
||||
if (!runtime || !normalizedName || objectId == null || version === -1) {
|
||||
return undefined;
|
||||
}
|
||||
const object = runtime.state.objectsById.get(objectId);
|
||||
return object ? { ...object } : undefined;
|
||||
return useMemo(() => {
|
||||
if (!runtime || !normalizedName || objectId == null || version === -1) {
|
||||
return undefined;
|
||||
}
|
||||
const object = runtime.state.objectsById.get(objectId);
|
||||
return object ? { ...object } : undefined;
|
||||
}, [runtime, normalizedName, objectId, version]);
|
||||
}
|
||||
|
||||
export function useDatablockByName(
|
||||
|
|
@ -452,11 +455,13 @@ export function useDatablockByName(
|
|||
objectId == null ? -1 : (state.runtime.objectVersionById[objectId] ?? -1),
|
||||
);
|
||||
|
||||
if (!runtime || !normalizedName || objectId == null || version === -1) {
|
||||
return undefined;
|
||||
}
|
||||
const object = runtime.state.objectsById.get(objectId);
|
||||
return object ? { ...object } : undefined;
|
||||
return useMemo(() => {
|
||||
if (!runtime || !normalizedName || objectId == null || version === -1) {
|
||||
return undefined;
|
||||
}
|
||||
const object = runtime.state.objectsById.get(objectId);
|
||||
return object ? { ...object } : undefined;
|
||||
}, [runtime, normalizedName, objectId, version]);
|
||||
}
|
||||
|
||||
export function useRuntimeChildIds(
|
||||
|
|
|
|||
|
|
@ -29,6 +29,26 @@ export class LiveStreamAdapter extends StreamEngine {
|
|||
private _snapshot: StreamSnapshot | null = null;
|
||||
private _snapshotTick = -1;
|
||||
private _ready = false;
|
||||
|
||||
// Generation counters for HUD caching in buildSnapshot() — avoids
|
||||
// rebuilding arrays every tick when HUD state hasn't changed.
|
||||
private _teamScoresGen = 0;
|
||||
private _rosterGen = 0;
|
||||
private _weaponsHudGen = 0;
|
||||
private _inventoryHudGen = 0;
|
||||
private _cachedHud: {
|
||||
teamScoresGen: number;
|
||||
rosterGen: number;
|
||||
weaponsHudGen: number;
|
||||
inventoryHudGen: number;
|
||||
weaponsHud: StreamSnapshot["weaponsHud"];
|
||||
inventoryHud: StreamSnapshot["inventoryHud"];
|
||||
backpackPackIndex: number;
|
||||
backpackActive: boolean;
|
||||
backpackHud: StreamSnapshot["backpackHud"];
|
||||
teamScores: StreamSnapshot["teamScores"];
|
||||
playerRoster: StreamSnapshot["playerRoster"];
|
||||
} | null = null;
|
||||
/** Class names for datablocks, tracked from SimDataBlockEvents. */
|
||||
private dataBlockClassNames = new Map<number, string>();
|
||||
|
||||
|
|
@ -101,6 +121,24 @@ export class LiveStreamAdapter extends StreamEngine {
|
|||
return [...shapes];
|
||||
}
|
||||
|
||||
// ── Generation counter hooks ──
|
||||
|
||||
protected onTeamScoresChanged(): void {
|
||||
this._teamScoresGen++;
|
||||
}
|
||||
|
||||
protected onRosterChanged(): void {
|
||||
this._rosterGen++;
|
||||
}
|
||||
|
||||
protected onWeaponsHudChanged(): void {
|
||||
this._weaponsHudGen++;
|
||||
}
|
||||
|
||||
protected onInventoryHudChanged(): void {
|
||||
this._inventoryHudGen++;
|
||||
}
|
||||
|
||||
// ── StreamingPlayback interface ──
|
||||
|
||||
reset(): void {
|
||||
|
|
@ -109,6 +147,7 @@ export class LiveStreamAdapter extends StreamEngine {
|
|||
this.currentTimeSec = 0;
|
||||
this._snapshot = null;
|
||||
this._snapshotTick = -1;
|
||||
this._cachedHud = null;
|
||||
this.dataBlockClassNames.clear();
|
||||
this.observerMode = "fly";
|
||||
this.missionName = null;
|
||||
|
|
@ -206,6 +245,7 @@ export class LiveStreamAdapter extends StreamEngine {
|
|||
this._ready = false;
|
||||
this._snapshot = null;
|
||||
this._snapshotTick = -1;
|
||||
this._cachedHud = null;
|
||||
this.observerMode = "fly";
|
||||
this.lastMoveAck = 0;
|
||||
// Clear stale mission info — new values arrive via MsgClientReady
|
||||
|
|
@ -604,8 +644,46 @@ export class LiveStreamAdapter extends StreamEngine {
|
|||
const entities = this.buildEntityList();
|
||||
const timeSec = this.currentTimeSec;
|
||||
const { chatMessages, audioEvents } = this.buildTimeFilteredEvents(timeSec);
|
||||
const { weaponsHud, inventoryHud, backpackHud, teamScores, playerRoster } =
|
||||
this.buildHudState();
|
||||
|
||||
// Reuse cached HUD arrays when generation counters haven't changed.
|
||||
const prev = this._cachedHud;
|
||||
let weaponsHud: StreamSnapshot["weaponsHud"];
|
||||
let inventoryHud: StreamSnapshot["inventoryHud"];
|
||||
let backpackHud: StreamSnapshot["backpackHud"];
|
||||
let teamScores: StreamSnapshot["teamScores"];
|
||||
let playerRoster: StreamSnapshot["playerRoster"];
|
||||
|
||||
if (
|
||||
prev &&
|
||||
prev.weaponsHudGen === this._weaponsHudGen &&
|
||||
prev.inventoryHudGen === this._inventoryHudGen &&
|
||||
prev.teamScoresGen === this._teamScoresGen &&
|
||||
prev.rosterGen === this._rosterGen &&
|
||||
prev.backpackPackIndex === this.backpackHud.packIndex &&
|
||||
prev.backpackActive === this.backpackHud.active
|
||||
) {
|
||||
weaponsHud = prev.weaponsHud;
|
||||
inventoryHud = prev.inventoryHud;
|
||||
backpackHud = prev.backpackHud;
|
||||
teamScores = prev.teamScores;
|
||||
playerRoster = prev.playerRoster;
|
||||
} else {
|
||||
({ weaponsHud, inventoryHud, backpackHud, teamScores, playerRoster } =
|
||||
this.buildHudState());
|
||||
this._cachedHud = {
|
||||
weaponsHudGen: this._weaponsHudGen,
|
||||
inventoryHudGen: this._inventoryHudGen,
|
||||
teamScoresGen: this._teamScoresGen,
|
||||
rosterGen: this._rosterGen,
|
||||
backpackPackIndex: this.backpackHud.packIndex,
|
||||
backpackActive: this.backpackHud.active,
|
||||
weaponsHud,
|
||||
inventoryHud,
|
||||
backpackHud,
|
||||
teamScores,
|
||||
playerRoster,
|
||||
};
|
||||
}
|
||||
|
||||
// Default observer camera if none exists
|
||||
if (!this.camera) {
|
||||
|
|
|
|||
|
|
@ -115,7 +115,11 @@ varying vec3 vTerrainWorldPos;`,
|
|||
shader.vertexShader = shader.vertexShader.replace(
|
||||
"#include <worldpos_vertex>",
|
||||
`#include <worldpos_vertex>
|
||||
vTerrainWorldPos = (modelMatrix * vec4(transformed, 1.0)).xyz;`,
|
||||
vec4 _terrainPos = vec4(transformed, 1.0);
|
||||
#ifdef USE_INSTANCING
|
||||
_terrainPos = instanceMatrix * _terrainPos;
|
||||
#endif
|
||||
vTerrainWorldPos = (modelMatrix * _terrainPos).xyz;`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,8 +45,15 @@ const vertexShader = /* glsl */ `
|
|||
}
|
||||
|
||||
void main() {
|
||||
// Apply instance transform when using InstancedMesh.
|
||||
#ifdef USE_INSTANCING
|
||||
mat4 localModel = modelMatrix * instanceMatrix;
|
||||
#else
|
||||
mat4 localModel = modelMatrix;
|
||||
#endif
|
||||
|
||||
// Get world position for wave calculation
|
||||
vec4 worldPos = modelMatrix * vec4(position, 1.0);
|
||||
vec4 worldPos = localModel * vec4(position, 1.0);
|
||||
vWorldPosition = worldPos.xyz;
|
||||
|
||||
// Apply wave displacement to Y (vertical axis in Three.js)
|
||||
|
|
@ -55,7 +62,7 @@ const vertexShader = /* glsl */ `
|
|||
|
||||
// Calculate final world position after displacement for fog
|
||||
#ifdef USE_FOG
|
||||
vec4 displacedWorldPos = modelMatrix * vec4(displaced, 1.0);
|
||||
vec4 displacedWorldPos = localModel * vec4(displaced, 1.0);
|
||||
vFogWorldPosition = displacedWorldPos.xyz;
|
||||
#endif
|
||||
|
||||
|
|
@ -63,7 +70,7 @@ const vertexShader = /* glsl */ `
|
|||
vViewVector = cameraPosition - worldPos.xyz;
|
||||
vDistance = length(vViewVector);
|
||||
|
||||
vec4 mvPosition = viewMatrix * modelMatrix * vec4(displaced, 1.0);
|
||||
vec4 mvPosition = viewMatrix * localModel * vec4(displaced, 1.0);
|
||||
gl_Position = projectionMatrix * mvPosition;
|
||||
|
||||
// Set fog depth (distance from camera) - normally done by fog_vertex include
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue