feat: Optimize WorldBlocks rendering with chunked InstancedMesh and interaction handling

This commit is contained in:
MayaTheShy
2026-02-20 02:18:08 -05:00
parent e62e83cf33
commit 1ef975cbae

View File

@@ -541,23 +541,35 @@ function getBlockAppearance(blockName) {
return { color: '#666666', pattern: 'solid' }; return { color: '#666666', pattern: 'solid' };
} }
// WorldBlocks component to render discovered blocks with Minecraft textures // WorldBlocks component using chunked InstancedMesh rendering for performance
function WorldBlocks({ blocks }) { function WorldBlocks({ blocks, interactionMode, selectedTurtleId, onBlockClick }) {
const [hoveredBlock, setHoveredBlock] = useState(null); const [hoveredBlock, setHoveredBlock] = useState(null);
const { textures, multiFaceTextures, loading } = useMinecraftTextures(blocks); const { textures, multiFaceTextures, loading } = useMinecraftTextures(blocks);
// Group blocks by block name for better performance // Chunk size (16x16 like Minecraft)
const blockGroups = useMemo(() => { const CHUNK_SIZE = 16;
const groups = new Map();
// Group blocks into chunks and by block type within each chunk
const chunks = useMemo(() => {
const chunkMap = new Map();
blocks.forEach(block => { blocks.forEach(block => {
if (!groups.has(block.name)) { const chunkX = Math.floor(block.x / CHUNK_SIZE);
groups.set(block.name, []); const chunkZ = Math.floor(block.z / CHUNK_SIZE);
const chunkKey = `${chunkX},${chunkZ}`;
if (!chunkMap.has(chunkKey)) {
chunkMap.set(chunkKey, { key: chunkKey, chunkX, chunkZ, blocksByType: new Map() });
} }
groups.get(block.name).push(block);
const chunk = chunkMap.get(chunkKey);
if (!chunk.blocksByType.has(block.name)) {
chunk.blocksByType.set(block.name, []);
}
chunk.blocksByType.get(block.name).push(block);
}); });
return Array.from(groups.entries()); return Array.from(chunkMap.values());
}, [blocks]); }, [blocks]);
if (loading) { if (loading) {
@@ -566,84 +578,224 @@ function WorldBlocks({ blocks }) {
return ( return (
<group> <group>
{blockGroups.map(([blockName, blockList]) => { {chunks.map(chunk => (
const appearance = getBlockAppearance(blockName); <ChunkMesh
const texture = textures[blockName]; key={chunk.key}
const multiFace = multiFaceTextures[blockName]; chunk={chunk}
textures={textures}
return ( multiFaceTextures={multiFaceTextures}
<group key={blockName}> hoveredBlock={hoveredBlock}
{blockList.map((block) => { setHoveredBlock={setHoveredBlock}
const blockKey = `${block.x},${block.y},${block.z}`; interactionMode={interactionMode}
const isHovered = hoveredBlock === blockKey; selectedTurtleId={selectedTurtleId}
onBlockClick={onBlockClick}
return ( />
<group key={blockKey}> ))}
<mesh
position={[block.x, block.y, block.z]} {/* Hover label overlay */}
onPointerOver={(e) => { {hoveredBlock && (
e.stopPropagation(); <Text
setHoveredBlock(blockKey); position={[hoveredBlock.x, hoveredBlock.y + 1.0, hoveredBlock.z]}
}} fontSize={0.3}
onPointerOut={() => setHoveredBlock(null)} color="white"
> anchorX="center"
<boxGeometry args={[1.0, 1.0, 1.0]} /> anchorY="middle"
{multiFace ? ( outlineWidth={0.05}
// Multi-face block (different top/side/bottom) outlineColor="#000000"
// Box face order: +x, -x, +y, -y, +z, -z >
multiFace.map((faceTexture, i) => ( {hoveredBlock.name.replace('minecraft:', '').replace(/_/g, ' ').toUpperCase()}
<meshStandardMaterial {'\n'}
key={i} {`(${hoveredBlock.x}, ${hoveredBlock.y}, ${hoveredBlock.z})`}
attach={`material-${i}`} </Text>
map={faceTexture} )}
color="#ffffff"
emissive={appearance.emissive || '#000000'}
emissiveIntensity={isHovered ? 0.25 : (appearance.intensity || 0.02)}
roughness={0.8}
metalness={0.0}
transparent
opacity={isHovered ? 0.98 : 0.9}
/>
))
) : (
// Single texture or color fallback
<meshStandardMaterial
map={texture}
color={texture ? '#ffffff' : appearance.color}
emissive={appearance.emissive || appearance.color}
emissiveIntensity={isHovered ? (appearance.intensity || 0.05) + 0.2 : (appearance.intensity || 0.05)}
roughness={appearance.pattern === 'ore' ? 0.4 : 0.8}
metalness={appearance.pattern === 'ore' ? 0.2 : 0.0}
transparent
opacity={isHovered ? 0.98 : 0.9}
/>
)}
</mesh>
{/* Block label on hover */}
{isHovered && (
<Text
position={[block.x, block.y + 0.8, block.z]}
fontSize={0.3}
color="white"
anchorX="center"
anchorY="middle"
outlineWidth={0.05}
outlineColor="#000000"
>
{block.name.replace('minecraft:', '').replace(/_/g, ' ').toUpperCase()}
</Text>
)}
</group>
);
})}
</group>
);
})}
</group> </group>
); );
} }
// Individual chunk rendered with InstancedMesh per block type
function ChunkMesh({ chunk, textures, multiFaceTextures, hoveredBlock, setHoveredBlock, interactionMode, selectedTurtleId, onBlockClick }) {
return (
<group>
{Array.from(chunk.blocksByType.entries()).map(([blockName, blockList]) => (
<BlockTypeInstances
key={`${chunk.key}-${blockName}`}
blockName={blockName}
blockList={blockList}
textures={textures}
multiFaceTextures={multiFaceTextures}
hoveredBlock={hoveredBlock}
setHoveredBlock={setHoveredBlock}
interactionMode={interactionMode}
selectedTurtleId={selectedTurtleId}
onBlockClick={onBlockClick}
/>
))}
</group>
);
}
// InstancedMesh for all blocks of one type within a chunk
function BlockTypeInstances({ blockName, blockList, textures, multiFaceTextures, hoveredBlock, setHoveredBlock, interactionMode, selectedTurtleId, onBlockClick }) {
const meshRef = useRef();
const appearance = useMemo(() => getBlockAppearance(blockName), [blockName]);
const texture = textures[blockName];
const multiFace = multiFaceTextures[blockName];
const tempMatrix = useMemo(() => new THREE.Matrix4(), []);
const tempColor = useMemo(() => new THREE.Color(), []);
// Set instance matrices
useEffect(() => {
if (!meshRef.current) return;
blockList.forEach((block, i) => {
tempMatrix.setPosition(block.x, block.y, block.z);
meshRef.current.setMatrixAt(i, tempMatrix);
// Default color (white for textured, appearance color for untextured)
tempColor.set(texture || multiFace ? '#ffffff' : appearance.color);
meshRef.current.setColorAt(i, tempColor);
});
meshRef.current.instanceMatrix.needsUpdate = true;
if (meshRef.current.instanceColor) meshRef.current.instanceColor.needsUpdate = true;
}, [blockList, texture, multiFace, appearance, tempMatrix, tempColor]);
// Update hover highlighting
useEffect(() => {
if (!meshRef.current || !meshRef.current.instanceColor) return;
blockList.forEach((block, i) => {
const isHovered = hoveredBlock &&
hoveredBlock.x === block.x &&
hoveredBlock.y === block.y &&
hoveredBlock.z === block.z;
if (isHovered) {
tempColor.set('#ffffff');
meshRef.current.setColorAt(i, tempColor);
} else {
tempColor.set(texture || multiFace ? '#ffffff' : appearance.color);
meshRef.current.setColorAt(i, tempColor);
}
});
meshRef.current.instanceColor.needsUpdate = true;
}, [hoveredBlock, blockList, texture, multiFace, appearance, tempColor]);
const handlePointerMove = useCallback((e) => {
e.stopPropagation();
const instanceId = e.instanceId;
if (instanceId !== undefined && instanceId < blockList.length) {
setHoveredBlock(blockList[instanceId]);
}
}, [blockList, setHoveredBlock]);
const handlePointerOut = useCallback(() => {
setHoveredBlock(null);
}, [setHoveredBlock]);
const handleClick = useCallback((e) => {
e.stopPropagation();
const instanceId = e.instanceId;
if (instanceId !== undefined && instanceId < blockList.length) {
const block = blockList[instanceId];
if (interactionMode === INTERACTION_MODE.MOVE && selectedTurtleId && onBlockClick) {
// In MOVE mode, click block surface to send turtle to adjacent position
const normal = e.face?.normal;
const target = {
x: block.x + (normal?.x || 0),
y: block.y + (normal?.y || 0),
z: block.z + (normal?.z || 0)
};
onBlockClick({ type: 'move', target, block });
} else if (interactionMode === INTERACTION_MODE.BUILD && onBlockClick) {
const normal = e.face?.normal;
const target = {
x: block.x + (normal?.x || 0),
y: block.y + (normal?.y || 0),
z: block.z + (normal?.z || 0)
};
onBlockClick({ type: 'build', target, block });
} else if (interactionMode === INTERACTION_MODE.SELECT && onBlockClick) {
onBlockClick({ type: 'select', block });
}
}
}, [blockList, interactionMode, selectedTurtleId, onBlockClick]);
// For multi-face blocks, we can't easily use InstancedMesh with different material per face,
// so we fall back to individual meshes for those (they're fewer — grass, logs, etc.)
if (multiFace) {
return (
<group>
{blockList.map((block) => {
const blockKey = `${block.x},${block.y},${block.z}`;
const isHovered = hoveredBlock &&
hoveredBlock.x === block.x &&
hoveredBlock.y === block.y &&
hoveredBlock.z === block.z;
return (
<mesh
key={blockKey}
position={[block.x, block.y, block.z]}
onPointerOver={(e) => { e.stopPropagation(); setHoveredBlock(block); }}
onPointerOut={() => setHoveredBlock(null)}
onClick={(e) => {
e.stopPropagation();
if (interactionMode === INTERACTION_MODE.MOVE && selectedTurtleId && onBlockClick) {
const normal = e.face?.normal;
onBlockClick({ type: 'move', target: { x: block.x + (normal?.x || 0), y: block.y + (normal?.y || 0), z: block.z + (normal?.z || 0) }, block });
} else if (interactionMode === INTERACTION_MODE.BUILD && onBlockClick) {
const normal = e.face?.normal;
onBlockClick({ type: 'build', target: { x: block.x + (normal?.x || 0), y: block.y + (normal?.y || 0), z: block.z + (normal?.z || 0) }, block });
} else if (interactionMode === INTERACTION_MODE.SELECT && onBlockClick) {
onBlockClick({ type: 'select', block });
}
}}
>
<boxGeometry args={[1.0, 1.0, 1.0]} />
{multiFace.map((faceTexture, i) => (
<meshStandardMaterial
key={i}
attach={`material-${i}`}
map={faceTexture}
color="#ffffff"
emissive={appearance.emissive || '#000000'}
emissiveIntensity={isHovered ? 0.25 : (appearance.intensity || 0.02)}
roughness={0.8}
metalness={0.0}
transparent
opacity={isHovered ? 0.98 : 0.9}
/>
))}
</mesh>
);
})}
</group>
);
}
return (
<instancedMesh
ref={meshRef}
args={[null, null, blockList.length]}
onPointerMove={handlePointerMove}
onPointerOut={handlePointerOut}
onClick={handleClick}
>
<boxGeometry args={[1.0, 1.0, 1.0]} />
<meshStandardMaterial
map={texture}
color={texture ? '#ffffff' : appearance.color}
emissive={appearance.emissive || appearance.color}
emissiveIntensity={appearance.intensity || 0.05}
roughness={appearance.pattern === 'ore' ? 0.4 : 0.8}
metalness={appearance.pattern === 'ore' ? 0.2 : 0.0}
transparent
opacity={0.9}
/>
</instancedMesh>
);
}
// Path trail component (old version removed above) // Path trail component (old version removed above)
function OldPathTrail({ turtle }) { function OldPathTrail({ turtle }) {
const { position, homePosition } = turtle; const { position, homePosition } = turtle;