ThreeJS 纹理贴图创建一个我的世界草地方块
开始准备使用ThreeJS写一个类似《我的世界》场景的射击类游戏,地形和我的世界很相似。场景中需要进行很多的纹理贴图,本篇文章主要以给一个立方体贴图成草地为例子介绍 ThreeJS 中如何添加纹理?如何解决纹理贴图后方块不展示(纹理未生效,效果是黑色方块)问题?
给 mesh 增加纹理,实现草地方块
把大象装进冰箱需要三步,这里实现一个草地方块也需要三步。
**step one :初始化一个 geometry 立方体形状。
step two: 初始化纹理加载器,加载纹理。
step three: 将纹理贴到立方体上,渲染出来。
**
step one 初始化一个111 的立方体
前边两篇文章中也有介绍,尤其是第一篇 【渲染第一个ThreeJS立方体】。不做详细介绍呀,一行代码
const geometry = new THREE.BoxGeometry();
Step two 初始化纹理加载器,加载纹理
开始介绍之前我们先简单减少一下 ThreeJS 支持的纹理加载器以及其加载的纹理类型,ThreeJS 提供 TextureLoader 来加载静态图像纹理;CubeTextureLoader 用于加载立方体贴图纹理,它通过加载 6 个图像来作为立方体的六个面,常用来创建天空盒天空球效果;CompressedTextureLoader 用于加载压缩过后的纹理; DataTextureLoader 用于加载像素数据组成的纹理,常用于动态生成纹理或者使用特定的纹理生成算饭来创建纹理。还有一些其他通用的加载器用于加载文件、视频、音频等资源。
本文选择使用 TextureLoader 来加载3张静态图片分别作为不同方向的纹理。开始前先准备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 }
step two 完成,到目前已经完成大部分了,接下来只要将 grassBlock.material
用于新建的立方体上,然后将立方体渲染出来即可。
Step three 使用纹理材质创建立方体并渲染
这一步我们需要进行一些封装,目的是将职责进行隔离。将创建立方体的代码放到 generateFrag.ts 中;我们将 scene 的初始化抽离到一个固定的类 Core 中进行封装,Core 类主要处理几件事情:初始化 scene、初始化 camera、初始化渲染器 renderer。
// 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 来处理一个盒子需要多张图片来作为材质时 Promise.all 会很好用。如果是单张图片可直接监听onload 事件 loader.load(imgpath, onload)
我们在 core.js 中使用 requestAnimationFrame 来重复渲染草地格子第二次渲染开始纹理已经加载完成了由此避开了纹理未加载就渲染导致形状黑色问题。后续游戏中使用的纹理大部分都集中加载,因此可以检测每个材质回来后就进行新的渲染触发。
为场景添加合适的光源
想象一下在漆黑的屋子里面有一个彩色的球,一点光都没有啥都看不见。另外就是逆光时我们看不见光源背后的东西。因此我们需要将光源设置到相机的顺方向(或者多设置几组光源),保证相机与物体的连线上能存在光的分量。
本文由mdnice多平台发布