ThreeJS 纹理贴图创建一个我的世界草地方块

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多平台发布

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,104评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,816评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,697评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,836评论 1 298
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,851评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,441评论 1 310
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,992评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,899评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,457评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,529评论 3 341
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,664评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,346评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,025评论 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,511评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,611评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,081评论 3 377
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,675评论 2 359

推荐阅读更多精彩内容