From 1ef975cbae0b16feb59b56b5d9c77fa68b02745f Mon Sep 17 00:00:00 2001 From: MayaTheShy Date: Fri, 20 Feb 2026 02:18:08 -0500 Subject: [PATCH] feat: Optimize WorldBlocks rendering with chunked InstancedMesh and interaction handling --- client/src/components/Map3D.jsx | 318 +++++++++++++++++++++++--------- 1 file changed, 235 insertions(+), 83 deletions(-) diff --git a/client/src/components/Map3D.jsx b/client/src/components/Map3D.jsx index 2368625..e64e585 100644 --- a/client/src/components/Map3D.jsx +++ b/client/src/components/Map3D.jsx @@ -541,23 +541,35 @@ function getBlockAppearance(blockName) { return { color: '#666666', pattern: 'solid' }; } -// WorldBlocks component to render discovered blocks with Minecraft textures -function WorldBlocks({ blocks }) { +// WorldBlocks component using chunked InstancedMesh rendering for performance +function WorldBlocks({ blocks, interactionMode, selectedTurtleId, onBlockClick }) { const [hoveredBlock, setHoveredBlock] = useState(null); const { textures, multiFaceTextures, loading } = useMinecraftTextures(blocks); - // Group blocks by block name for better performance - const blockGroups = useMemo(() => { - const groups = new Map(); + // Chunk size (16x16 like Minecraft) + const CHUNK_SIZE = 16; + + // Group blocks into chunks and by block type within each chunk + const chunks = useMemo(() => { + const chunkMap = new Map(); blocks.forEach(block => { - if (!groups.has(block.name)) { - groups.set(block.name, []); + const chunkX = Math.floor(block.x / CHUNK_SIZE); + 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]); if (loading) { @@ -566,84 +578,224 @@ function WorldBlocks({ blocks }) { return ( - {blockGroups.map(([blockName, blockList]) => { - const appearance = getBlockAppearance(blockName); - const texture = textures[blockName]; - const multiFace = multiFaceTextures[blockName]; - - return ( - - {blockList.map((block) => { - const blockKey = `${block.x},${block.y},${block.z}`; - const isHovered = hoveredBlock === blockKey; - - return ( - - { - e.stopPropagation(); - setHoveredBlock(blockKey); - }} - onPointerOut={() => setHoveredBlock(null)} - > - - {multiFace ? ( - // Multi-face block (different top/side/bottom) - // Box face order: +x, -x, +y, -y, +z, -z - multiFace.map((faceTexture, i) => ( - - )) - ) : ( - // Single texture or color fallback - - )} - - - {/* Block label on hover */} - {isHovered && ( - - {block.name.replace('minecraft:', '').replace(/_/g, ' ').toUpperCase()} - - )} - - ); - })} - - ); - })} + {chunks.map(chunk => ( + + ))} + + {/* Hover label overlay */} + {hoveredBlock && ( + + {hoveredBlock.name.replace('minecraft:', '').replace(/_/g, ' ').toUpperCase()} + {'\n'} + {`(${hoveredBlock.x}, ${hoveredBlock.y}, ${hoveredBlock.z})`} + + )} ); } +// Individual chunk rendered with InstancedMesh per block type +function ChunkMesh({ chunk, textures, multiFaceTextures, hoveredBlock, setHoveredBlock, interactionMode, selectedTurtleId, onBlockClick }) { + return ( + + {Array.from(chunk.blocksByType.entries()).map(([blockName, blockList]) => ( + + ))} + + ); +} + +// 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 ( + + {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 ( + { 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 }); + } + }} + > + + {multiFace.map((faceTexture, i) => ( + + ))} + + ); + })} + + ); + } + + return ( + + + + + ); +} + // Path trail component (old version removed above) function OldPathTrail({ turtle }) { const { position, homePosition } = turtle;