feat: Optimize WorldBlocks rendering with chunked InstancedMesh and interaction handling
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user