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' };
}
// 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 (
<group>
{blockGroups.map(([blockName, blockList]) => {
const appearance = getBlockAppearance(blockName);
const texture = textures[blockName];
const multiFace = multiFaceTextures[blockName];
return (
<group key={blockName}>
{blockList.map((block) => {
const blockKey = `${block.x},${block.y},${block.z}`;
const isHovered = hoveredBlock === blockKey;
return (
<group key={blockKey}>
<mesh
position={[block.x, block.y, block.z]}
onPointerOver={(e) => {
e.stopPropagation();
setHoveredBlock(blockKey);
}}
onPointerOut={() => setHoveredBlock(null)}
>
<boxGeometry args={[1.0, 1.0, 1.0]} />
{multiFace ? (
// Multi-face block (different top/side/bottom)
// Box face order: +x, -x, +y, -y, +z, -z
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}
/>
))
) : (
// 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>
);
})}
{chunks.map(chunk => (
<ChunkMesh
key={chunk.key}
chunk={chunk}
textures={textures}
multiFaceTextures={multiFaceTextures}
hoveredBlock={hoveredBlock}
setHoveredBlock={setHoveredBlock}
interactionMode={interactionMode}
selectedTurtleId={selectedTurtleId}
onBlockClick={onBlockClick}
/>
))}
{/* Hover label overlay */}
{hoveredBlock && (
<Text
position={[hoveredBlock.x, hoveredBlock.y + 1.0, hoveredBlock.z]}
fontSize={0.3}
color="white"
anchorX="center"
anchorY="middle"
outlineWidth={0.05}
outlineColor="#000000"
>
{hoveredBlock.name.replace('minecraft:', '').replace(/_/g, ' ').toUpperCase()}
{'\n'}
{`(${hoveredBlock.x}, ${hoveredBlock.y}, ${hoveredBlock.z})`}
</Text>
)}
</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)
function OldPathTrail({ turtle }) {
const { position, homePosition } = turtle;