Three.js 使用纹理贴图创建 Minecraft 草地方块及常见问题解决
本文介绍如何使用 Three.js 的 TextureLoader 加载静态图像,为立方体网格(Mesh)应用草地纹理以模拟 Minecraft 风格的地形。重点解析了贴图后显示为黑色方块的两个主要原因:异步加载时序问题与光源缺失,并提供了基于 Promise 或 onload 事件的处理方案及光源设置建议。
Three.js 纹理贴图创建 Minecraft 草地方块
在开发类似《我的世界》风格的射击游戏时,地形构建需要大量纹理贴图。本文以制作草地方块为例,详细说明如何在 Three.js 中添加纹理,并解决因纹理未生效导致方块显示为黑色的常见问题。

为网格(Mesh)添加纹理实现草地方块效果
实现一个带有纹理的草地方块主要包含三个步骤。
第一步:初始化一个立方体几何体(Geometry)。
第二步:初始化纹理加载器并加载纹理资源。
第三步:将纹理应用于立方体并进行渲染。
step one 初始化一个111 的立方体
关于立方体的初始化,前文已有介绍,此处不再赘述,仅需一行代码即可完成。
const geometry = new THREE.BoxGeometry();第二步:初始化纹理加载器并加载纹理
在操作前,简要回顾 Three.js 支持的纹理加载器及其类型:TextureLoader 用于加载静态图像;CubeTextureLoader 用于加载立方体贴图(如天空盒),需加载 6 张图像;CompressedTextureLoader 用于加载压缩纹理;DataTextureLoader 用于加载像素数据,常用于动态生成或特定算法生成的纹理。此外还有用于加载文件、视频、音频等的通用加载器。
本文选用 TextureLoader 加载 3 张静态图片,分别对应草地方块不同方向的纹理。草地方块共 6 个面:顶部为草坪纹理,侧面 4 个面共用一张纹理,底部为泥土纹理(具体图片资源见文末链接,请勿使用截图作为资源)。
准备所需的静态图片资源:
底部纹理:

侧面纹理:

顶部纹理:

将所有资源加载逻辑封装在 loader.ts 文件中处理。
import * as THREE from 'three';
// 导入静态的图片资源,位置注意是自己项目中存放静态资源的地址。
import grassBlockTextureSideImg from '../../assets/textures/blocks-clipped/grassBlockSide.png';
import grassBlockTextureTopImg from '../../assets/textures/blocks-clipped/grassBlockTop.png';
import dirtTextureImg from '../../assets/textures/blocks-clipped/dirt.png';
// 创建一个 THREE 加载器
const loader = new THREE.TextureLoader();
// 使用 loader.load 将静态图片加载到 ThreeJS 中
const grassBlockTextureSide = loader.load(grassBlockTextureSideImg);
const grassBlockTextureTop = loader.load(grassBlockTextureTopImg);
const dirtTexture = loader.load(dirtTextureImg);
// 定义清楚草地方块纹理顺序
export const grassBlock = {
name: 'grassBlock',
// 注意顺序
textureImg: [
grassBlockTextureSide,
grassBlockTextureSide,
grassBlockTextureTop,
dirtTexture,
grassBlockTextureSide,
grassBlockTextureSide,
],
material: [],
};
// 使用 THREE.MeshStandardMaterial 将纹理创建成材质 存入 grassBlock.material 上
grassBlock.material = grassBlock.textureImg.map((img, i) => {
return new THREE.MeshStandardMaterial({
map: img,
// side: THREE.DoubleSide
})
});
export default { grassBlock }第二步完成。接下来只需将 grassBlock.material 应用于新建的立方体,并完成渲染即可。
第三步:使用纹理材质创建立方体并渲染
此步骤涉及代码封装以实现职责分离。创建立方体的代码放入 generateFrag.ts;场景(scene)、相机(camera)及渲染器(renderer)的初始化封装在 Core 类中。
// generateFrag.ts
import Terrain from '.';
import * as THREE from 'three';
// 导入草地格子的配置数据
import { grassBlock } from '../controller/loader';
export default class GenerateFrags {
private terrain: Terrain;
constructor(terrain: Terrain) {
this.terrain = terrain;
}
generateAll() {}
// 主要关注这里
generateOneFrag() {
const geometry = new THREE.BoxGeometry(1, 1, 1);
// 使用纹理创建的材质来作为 mesh 的材质
const material = grassBlock.material;
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(0, 0, 1);
return mesh;
}
}core.ts 负责场景、相机、渲染器的初始化以及草地格子的渲染逻辑。
// core.ts
import * as THREE from 'three';
import Terrain from '../terrain';
import GenerateFrags from '../terrain/generateFrag';
export default class Core {
// scene
public scene: THREE.Scene;
// 透视相机
public camera: THREE.PerspectiveCamera;
// renderer 渲染器
public renderer: THREE.WebglRenderer;
// 地形对象
public terrain: Terrain;
constructor() {
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000,
);
this.renderer = new THREE.WebGLRenderer();
// 地形
this.terrain = new Terrain(this);
// 其他初始化操作一并处理
this.#init();
}
/**
* 1, 监听页面窗口大小改变,改变时需要个更新坐标系(相机位置)
*/
#init() {
window.addEventListener('resize', () => {
this.camera.aspect = window.innerHeight / window.innerWidth;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
});
// 初始化设置相机
// this.camera.fov = 80;
// this.camera.aspect = window.innerWidth / window.innerWidth;
// this.camera.far = 500;
// this.camera.updateProjectionMatrix();
this.camera.position.set(0, 0, 10);
// 初始化场景scene 背景
const backgroundColor = 0x87ceeb;
this.scene.fog = new THREE.FogExp2(0.02);
this.scene.background = new THREE.Color(backgroundColor);
// 初始化场景的灯光
const sunLight = new THREE.PointLight(0xffffff, 0.5);
sunLight.position.set(500, 500, 500);
this.scene.add(sunLight);
const sunLight2 = new THREE.PointLight(0xffffff, 0.2);
sunLight2.position.set(-500, 500, -500);
this.scene.add(sunLight2);
const reflectionLight = new THREE.AmbientLight(0x404040);
this.scene.add(reflectionLight);
this.renderer.setSize(window.innerWidth, window.innerHeight);
document
.getElementById('game-container')
.appendChild(this.renderer.domElement);
// 这里是调用入口
this.testRenderOneGrassBlock();
}
// 测试生成一个草地格子
testRenderOneGrassBlock() {
// 初始化一个 GenerateFrags 对象来创建一个草地格子
const generateOneFrag = new GenerateFrags(this.terrain);
const cube = generateOneFrag.generateOneFrag();
// 将草地格子加到 scene 中
this.scene.add(cube);
const animate = () => {
requestAnimationFrame(animate);
// 使用 mesh 的 rotation 来让草地格子旋转起来
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
// 调用渲染器进行渲染
this.renderer.render(this.scene, this.camera);
};
animate();
}
}至此,一个旋转的草地方块生成完毕,效果如下:

分析添加纹理后不生效(显示黑色方块)的原因
若图片创建逻辑正确但渲染结果为黑色方块,通常有两个原因:一是纹理异步加载,在渲染时纹理尚未加载完成;二是场景中缺少光源。

解决思路
确保渲染操作在纹理加载完成后进行。
若仅渲染一次,需保证渲染时纹理已就绪。对于多张图片材质的盒子,使用 Promise.all 处理最佳;若为单张图片,可直接监听 onload 事件 loader.load(imgpath, onload)。
在 core.js 中使用 requestAnimationFrame 进行重复渲染。由于第二次渲染时纹理已加载完成,从而避免了因纹理未加载导致的黑色方块问题。后续游戏中若纹理集中加载,可在每个材质加载完成后触发新的渲染。
为场景添加合适的光源
如同在黑暗房间中无法看见彩色物体,若无光照或处于逆光位置,也无法看到物体背面。因此,需将光源设置在相机前方(或设置多组光源),确保相机与物体连线方向上有光照分量。
以上为 Three.js 使用纹理贴图创建 Minecraft 草地方块的详细内容,更多 Three.js 纹理贴图资料请查阅本站相关文章。
Mock.js 拦截失效或数据格式错误的常见原因及排查
2026-11-05 10:11:59
Java和JavaScript名字像但没关系:辨析Java、JavaWeb与JS
2026-11-04 09:30:54
CodePen、JSFiddle与RunJS对比:哪款JS在线运行工具更适合你?
2026-11-03 16:46:57
JS 统计图渲染空白或报错怎么排查?常见问题解决指南
2026-11-03 09:49:40
flv.js 常见报错 MediaError 与解码失败排查指南
2026-11-02 10:29:29
