feat: Implement Minecraft texture loading and enhance WorldBlocks rendering with textures
This commit is contained in:
@@ -1,9 +1,129 @@
|
|||||||
import React, { useRef, useMemo, useEffect } from 'react';
|
import React, { useRef, useMemo, useEffect, useState } from 'react';
|
||||||
import { Canvas, useFrame } from '@react-three/fiber';
|
import { Canvas, useFrame, useLoader } from '@react-three/fiber';
|
||||||
import { OrbitControls, Grid, Text, Line } from '@react-three/drei';
|
import { OrbitControls, Grid, Text, Line } from '@react-three/drei';
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { useTurtleStore } from '../store/turtleStore';
|
import { useTurtleStore } from '../store/turtleStore';
|
||||||
|
|
||||||
|
// Texture mapping for Minecraft blocks
|
||||||
|
const TEXTURE_MAP = {
|
||||||
|
// Basic blocks
|
||||||
|
'minecraft:stone': 'stone',
|
||||||
|
'minecraft:cobblestone': 'cobblestone',
|
||||||
|
'minecraft:dirt': 'dirt',
|
||||||
|
'minecraft:grass_block': 'grass_block_side',
|
||||||
|
'minecraft:sand': 'sand',
|
||||||
|
'minecraft:red_sand': 'red_sand',
|
||||||
|
'minecraft:gravel': 'gravel',
|
||||||
|
'minecraft:bedrock': 'bedrock',
|
||||||
|
'minecraft:oak_log': 'oak_log',
|
||||||
|
'minecraft:oak_planks': 'oak_planks',
|
||||||
|
'minecraft:glass': 'glass',
|
||||||
|
|
||||||
|
// Stone variants
|
||||||
|
'minecraft:granite': 'granite',
|
||||||
|
'minecraft:diorite': 'diorite',
|
||||||
|
'minecraft:andesite': 'andesite',
|
||||||
|
'minecraft:deepslate': 'deepslate',
|
||||||
|
'minecraft:tuff': 'tuff',
|
||||||
|
'minecraft:calcite': 'calcite',
|
||||||
|
'minecraft:dripstone_block': 'dripstone_block',
|
||||||
|
|
||||||
|
// Ores (overworld)
|
||||||
|
'minecraft:coal_ore': 'coal_ore',
|
||||||
|
'minecraft:iron_ore': 'iron_ore',
|
||||||
|
'minecraft:gold_ore': 'gold_ore',
|
||||||
|
'minecraft:diamond_ore': 'diamond_ore',
|
||||||
|
'minecraft:emerald_ore': 'emerald_ore',
|
||||||
|
'minecraft:redstone_ore': 'redstone_ore',
|
||||||
|
'minecraft:lapis_ore': 'lapis_ore',
|
||||||
|
'minecraft:copper_ore': 'copper_ore',
|
||||||
|
|
||||||
|
// Deepslate ores
|
||||||
|
'minecraft:deepslate_coal_ore': 'deepslate_coal_ore',
|
||||||
|
'minecraft:deepslate_iron_ore': 'deepslate_iron_ore',
|
||||||
|
'minecraft:deepslate_gold_ore': 'deepslate_gold_ore',
|
||||||
|
'minecraft:deepslate_diamond_ore': 'deepslate_diamond_ore',
|
||||||
|
'minecraft:deepslate_emerald_ore': 'deepslate_emerald_ore',
|
||||||
|
'minecraft:deepslate_redstone_ore': 'deepslate_redstone_ore',
|
||||||
|
'minecraft:deepslate_lapis_ore': 'deepslate_lapis_ore',
|
||||||
|
'minecraft:deepslate_copper_ore': 'deepslate_copper_ore',
|
||||||
|
|
||||||
|
// Special blocks
|
||||||
|
'minecraft:obsidian': 'obsidian',
|
||||||
|
'minecraft:netherrack': 'netherrack',
|
||||||
|
'minecraft:soul_sand': 'soul_sand',
|
||||||
|
'minecraft:glowstone': 'glowstone',
|
||||||
|
'minecraft:end_stone': 'end_stone',
|
||||||
|
'minecraft:water': 'water_still',
|
||||||
|
'minecraft:lava': 'lava_still',
|
||||||
|
|
||||||
|
// Building blocks
|
||||||
|
'minecraft:bricks': 'bricks',
|
||||||
|
'minecraft:stone_bricks': 'stone_bricks',
|
||||||
|
'minecraft:mossy_cobblestone': 'mossy_cobblestone',
|
||||||
|
'minecraft:mossy_stone_bricks': 'mossy_stone_bricks',
|
||||||
|
'minecraft:prismarine': 'prismarine',
|
||||||
|
'minecraft:dark_prismarine': 'dark_prismarine',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to get texture URL from a Minecraft texture service
|
||||||
|
function getTextureURL(blockName) {
|
||||||
|
const textureName = TEXTURE_MAP[blockName] || 'stone';
|
||||||
|
// Using a public Minecraft texture API
|
||||||
|
return `https://raw.githubusercontent.com/InventivetalentDev/minecraft-assets/1.20.4/assets/minecraft/textures/block/${textureName}.png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom hook to load multiple textures
|
||||||
|
function useMinecraftTextures(blocks) {
|
||||||
|
const [textures, setTextures] = useState({});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const textureLoader = new THREE.TextureLoader();
|
||||||
|
const uniqueBlocks = [...new Set(blocks.map(b => b.name))];
|
||||||
|
const loadedTextures = {};
|
||||||
|
let loadedCount = 0;
|
||||||
|
|
||||||
|
if (uniqueBlocks.length === 0) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uniqueBlocks.forEach(blockName => {
|
||||||
|
const url = getTextureURL(blockName);
|
||||||
|
|
||||||
|
textureLoader.load(
|
||||||
|
url,
|
||||||
|
(texture) => {
|
||||||
|
texture.magFilter = THREE.NearestFilter;
|
||||||
|
texture.minFilter = THREE.NearestFilter;
|
||||||
|
texture.wrapS = THREE.RepeatWrapping;
|
||||||
|
texture.wrapT = THREE.RepeatWrapping;
|
||||||
|
loadedTextures[blockName] = texture;
|
||||||
|
loadedCount++;
|
||||||
|
|
||||||
|
if (loadedCount === uniqueBlocks.length) {
|
||||||
|
setTextures(loadedTextures);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
(error) => {
|
||||||
|
console.warn(`Failed to load texture for ${blockName}, using color fallback`);
|
||||||
|
loadedCount++;
|
||||||
|
|
||||||
|
if (loadedCount === uniqueBlocks.length) {
|
||||||
|
setTextures(loadedTextures);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [blocks]);
|
||||||
|
|
||||||
|
return { textures, loading };
|
||||||
|
}
|
||||||
|
|
||||||
// Minecraft-style turtle model
|
// Minecraft-style turtle model
|
||||||
function TurtleModel({ turtle, isSelected, onClick }) {
|
function TurtleModel({ turtle, isSelected, onClick }) {
|
||||||
const groupRef = useRef();
|
const groupRef = useRef();
|
||||||
@@ -165,77 +285,84 @@ function getBlockAppearance(blockName) {
|
|||||||
return { color: '#666666', pattern: 'solid' };
|
return { color: '#666666', pattern: 'solid' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// WorldBlocks component to render discovered blocks
|
// WorldBlocks component to render discovered blocks with Minecraft textures
|
||||||
function WorldBlocks({ blocks }) {
|
function WorldBlocks({ blocks }) {
|
||||||
const [hoveredBlock, setHoveredBlock] = React.useState(null);
|
const [hoveredBlock, setHoveredBlock] = useState(null);
|
||||||
|
const { textures, loading } = useMinecraftTextures(blocks);
|
||||||
|
|
||||||
// Group blocks by appearance for better performance
|
// Group blocks by block name for better performance
|
||||||
const blockGroups = useMemo(() => {
|
const blockGroups = useMemo(() => {
|
||||||
const groups = new Map();
|
const groups = new Map();
|
||||||
|
|
||||||
blocks.forEach(block => {
|
blocks.forEach(block => {
|
||||||
const appearance = getBlockAppearance(block.name);
|
if (!groups.has(block.name)) {
|
||||||
const key = `${appearance.color}-${appearance.pattern}`;
|
groups.set(block.name, []);
|
||||||
|
|
||||||
if (!groups.has(key)) {
|
|
||||||
groups.set(key, { appearance, blocks: [] });
|
|
||||||
}
|
}
|
||||||
groups.get(key).blocks.push(block);
|
groups.get(block.name).push(block);
|
||||||
});
|
});
|
||||||
|
|
||||||
return Array.from(groups.values());
|
return Array.from(groups.entries());
|
||||||
}, [blocks]);
|
}, [blocks]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Text position={[0, 5, 0]} fontSize={0.5} color="white">Loading textures...</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group>
|
<group>
|
||||||
{blockGroups.map((group, groupIdx) => (
|
{blockGroups.map(([blockName, blockList]) => {
|
||||||
<group key={groupIdx}>
|
const appearance = getBlockAppearance(blockName);
|
||||||
{group.blocks.map((block) => {
|
const texture = textures[blockName];
|
||||||
const appearance = group.appearance;
|
|
||||||
const blockKey = `${block.x},${block.y},${block.z}`;
|
return (
|
||||||
const isHovered = hoveredBlock === blockKey;
|
<group key={blockName}>
|
||||||
|
{blockList.map((block) => {
|
||||||
return (
|
const blockKey = `${block.x},${block.y},${block.z}`;
|
||||||
<group key={blockKey}>
|
const isHovered = hoveredBlock === blockKey;
|
||||||
<mesh
|
|
||||||
position={[block.x, block.y, block.z]}
|
return (
|
||||||
onPointerOver={(e) => {
|
<group key={blockKey}>
|
||||||
e.stopPropagation();
|
<mesh
|
||||||
setHoveredBlock(blockKey);
|
position={[block.x, block.y, block.z]}
|
||||||
}}
|
onPointerOver={(e) => {
|
||||||
onPointerOut={() => setHoveredBlock(null)}
|
e.stopPropagation();
|
||||||
>
|
setHoveredBlock(blockKey);
|
||||||
<boxGeometry args={[1.0, 1.0, 1.0]} />
|
}}
|
||||||
<meshStandardMaterial
|
onPointerOut={() => setHoveredBlock(null)}
|
||||||
color={appearance.color}
|
|
||||||
emissive={appearance.emissive || appearance.color}
|
|
||||||
emissiveIntensity={isHovered ? (appearance.intensity || 0.05) + 0.3 : (appearance.intensity || 0.05)}
|
|
||||||
roughness={appearance.pattern === 'ore' ? 0.4 : 0.8}
|
|
||||||
metalness={appearance.pattern === 'ore' ? 0.3 : 0.0}
|
|
||||||
transparent
|
|
||||||
opacity={isHovered ? 0.95 : 0.85}
|
|
||||||
/>
|
|
||||||
</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()}
|
<boxGeometry args={[1.0, 1.0, 1.0]} />
|
||||||
</Text>
|
<meshStandardMaterial
|
||||||
)}
|
map={texture}
|
||||||
</group>
|
color={texture ? '#ffffff' : appearance.color}
|
||||||
);
|
emissive={appearance.emissive || appearance.color}
|
||||||
})}
|
emissiveIntensity={isHovered ? (appearance.intensity || 0.05) + 0.2 : (appearance.intensity || 0.05)}
|
||||||
</group>
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user