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' };
|
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}
|
||||||
|
multiFaceTextures={multiFaceTextures}
|
||||||
|
hoveredBlock={hoveredBlock}
|
||||||
|
setHoveredBlock={setHoveredBlock}
|
||||||
|
interactionMode={interactionMode}
|
||||||
|
selectedTurtleId={selectedTurtleId}
|
||||||
|
onBlockClick={onBlockClick}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
return (
|
{/* Hover label overlay */}
|
||||||
<group key={blockName}>
|
{hoveredBlock && (
|
||||||
{blockList.map((block) => {
|
<Text
|
||||||
const blockKey = `${block.x},${block.y},${block.z}`;
|
position={[hoveredBlock.x, hoveredBlock.y + 1.0, hoveredBlock.z]}
|
||||||
const isHovered = hoveredBlock === blockKey;
|
fontSize={0.3}
|
||||||
|
color="white"
|
||||||
return (
|
anchorX="center"
|
||||||
<group key={blockKey}>
|
anchorY="middle"
|
||||||
<mesh
|
outlineWidth={0.05}
|
||||||
position={[block.x, block.y, block.z]}
|
outlineColor="#000000"
|
||||||
onPointerOver={(e) => {
|
>
|
||||||
e.stopPropagation();
|
{hoveredBlock.name.replace('minecraft:', '').replace(/_/g, ' ').toUpperCase()}
|
||||||
setHoveredBlock(blockKey);
|
{'\n'}
|
||||||
}}
|
{`(${hoveredBlock.x}, ${hoveredBlock.y}, ${hoveredBlock.z})`}
|
||||||
onPointerOut={() => setHoveredBlock(null)}
|
</Text>
|
||||||
>
|
)}
|
||||||
<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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</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;
|
||||||
|
|||||||
Reference in New Issue
Block a user