简体   繁体   中英

Three.js: Cannot display mesh created with texture array

I'm building a very original game based in cubes you place in a sandbox world (a totally unique concept that will revolutionize gaming as we know it) and I'm working with the chunk generation. Here's what I have so far:

My blocks are defined in an object literal:

import * as THREE from 'three';

const loader = new THREE.TextureLoader();

interface BlockAttrs
{
    breakable: boolean;
    empty:     boolean;
}

export interface Block
{
    attrs:       BlockAttrs;
    mat_bottom?: THREE.MeshBasicMaterial;
    mat_side?:   THREE.MeshBasicMaterial;
    mat_top?:    THREE.MeshBasicMaterial;
}

interface BlockList
{
    [key: string]: Block;
}

export const Blocks: BlockList = {
    air:
    {
        attrs:
        {
            breakable: false,
            empty: true,
        },
    },
    grass:
    {
        attrs:
        {
            breakable: true,
            empty: false,
        },
        mat_bottom: new THREE.MeshBasicMaterial({map: loader.load("/tex/dirt.png")}),
        mat_side: new THREE.MeshBasicMaterial({map: loader.load("/tex/grass-side.png")}),
        mat_top: new THREE.MeshBasicMaterial({map: loader.load("/tex/grass-top.png")}),
    },
};

Here is my Chunk class:

import * as THREE from 'three';
import { BufferGeometryUtils } from 'three/examples/jsm/utils/BufferGeometryUtils';

import { Block, Blocks } from './blocks';

const px = 0; const nx = 1; const py = 2;
const ny = 3; const pz = 4; const nz = 5;

export default class Chunk
{
    private static readonly faces = [
        new THREE.PlaneGeometry(1,1)
            .rotateY(Math.PI / 2)
            .translate(0.5, 0, 0),

        new THREE.PlaneGeometry(1,1)
            .rotateY(-Math.PI / 2)
            .translate(-0.5, 0, 0),

        new THREE.PlaneGeometry(1,1)
            .rotateX(-Math.PI / 2)
            .translate(0, 0.5, 0),

        new THREE.PlaneGeometry(1,1)
            .rotateX(Math.PI / 2)
            .translate(0, -0.5, 0),

        new THREE.PlaneGeometry(1,1)
            .translate(0, 0, 0.5),

        new THREE.PlaneGeometry(1,1)
            .rotateY(Math.PI)
            .translate(0, 0, -0.5)
    ];
    private structure: Array<Array<Array<Block>>>;
    public static readonly size = 16;
    private materials = Array<THREE.MeshBasicMaterial>();
    private terrain = Array<THREE.BufferGeometry>();

    constructor ()
    {
        this.structure = new Array<Array<Array<Block>>>(Chunk.size);

        for (let x = 0; x < Chunk.size; x++)
        {
            this.structure[x] = new Array<Array<Block>>(Chunk.size);

            for (let y = 0; y < Chunk.size; y++)
            {
                this.structure[x][y] = new Array<Block>(Chunk.size);

                for (let z = 0; z < Chunk.size; z++)
                    if ((x+y+z) % 2)
                        this.structure[x][y][z] = Blocks.grass;
                    else
                        this.structure[x][y][z] = Blocks.air;
            }
        }
    }

    private blockEmpty (x: number, y: number, z: number): boolean
    {
        let empty = true;

        if (
            x >= 0 && x < Chunk.size &&
            y >= 0 && y < Chunk.size &&
            z >= 0 && z < Chunk.size
        ) {
            empty = this.structure[x][y][z].attrs.empty;
        }

        return empty;
    }

    private generateBlockFaces (x: number, y: number, z: number): void
    {
        if (this.blockEmpty(x+1, y, z))
        {
            this.terrain.push(Chunk.faces[px].clone().translate(x, y, z));
            this.materials.push(this.structure[x][y][z].mat_side);
        }

        if (this.blockEmpty(x, y, z+1))
        {
            this.terrain.push(Chunk.faces[nx].clone().translate(x, y, z));
            this.materials.push(this.structure[x][y][z].mat_side);
        }

        if (this.blockEmpty(x, y-1, z))
        {
            this.terrain.push(Chunk.faces[py].clone().translate(x, y, z));
            this.materials.push(this.structure[x][y][z].mat_bottom);
        }

        if (this.blockEmpty(x, y+1, z))
        {
            this.terrain.push(Chunk.faces[ny].clone().translate(x, y, z));
            this.materials.push(this.structure[x][y][z].mat_top);
        }

        if (this.blockEmpty(x, y, z-1))
        {
            this.terrain.push(Chunk.faces[pz].clone().translate(x, y, z));
            this.materials.push(this.structure[x][y][z].mat_side);
        }

        if (this.blockEmpty(x-1, y, z))
        {
            this.terrain.push(Chunk.faces[nz].clone().translate(x, y, z));
            this.materials.push(this.structure[x][y][z].mat_side);
        }
    }

    public generateTerrain (): THREE.Mesh
    {
        this.terrain   = new Array<THREE.BufferGeometry>();
        this.materials = new Array<THREE.MeshBasicMaterial>();

        for (let x = 0; x < Chunk.size; x++)
            for (let y = 0; y < Chunk.size; y++)
                for (let z = 0; z < Chunk.size; z++)
                    if (!this.structure[x][y][z].attrs.empty)
                        this.generateBlockFaces(x, y, z);

        return new THREE.Mesh(
            BufferGeometryUtils.mergeBufferGeometries(this.terrain),
            this.materials
        );
    }
}

I know the mesh creator should be decoupled from the model, but right now I'm experimenting. The class works like this:

First, constructor() creates a 3D matrix of Block . I've set it to create it in a chess board pattern of air and grass , so every other block is empty.

Next, I call generateTerrain() from my Scene:

this.chunk = new Chunk();
this.add(this.chunk.generateTerrain());

When this method is called, it enters generateBlockFaces for every non-empty block and pushes the appropiate PlaneGeometry s into the terrain array as well as the appropriate THREE.MeshBasicMaterial into the materials array. I then merge the geometries using BufferGeometryUtils.mergeBufferGeometries and create the mesh passing the merged geometry and the materials array.

The problem I have is that creating the mesh works perfectly well when passing new THREE.MeshNormalMaterial , or any other material for that matter, but not when I pass the materials array. Passing the array creates the object (and console.log ing it shows that it was created without errors), but it isn't drawn with the scene.

Am I mistaken in believing that the materials array will assign a material to each of the faces? What am I doing wrong?

I solved it after finding a reference to THREE.UVMapping in the docs. When sending the geometry to the GPU, textures coordinates need to be a biyection from the vertices coordinates. To achieve this, I defined the following three attributes in my blocks:

uv_bottom: [
    stone_row     / Textures.rows, (stone_col+1) / Textures.cols,
    (stone_row+1) / Textures.rows, (stone_col+1) / Textures.cols,
    stone_row     / Textures.rows, stone_col     / Textures.cols,
    (stone_row+1) / Textures.rows, stone_col     / Textures.cols,
],
uv_side: [
    stone_row     / Textures.rows, (stone_col+1) / Textures.cols,
    (stone_row+1) / Textures.rows, (stone_col+1) / Textures.cols,
    stone_row     / Textures.rows, stone_col     / Textures.cols,
    (stone_row+1) / Textures.rows, stone_col     / Textures.cols,
],
uv_top: [
    stone_row     / Textures.rows, (stone_col+1) / Textures.cols,
    (stone_row+1) / Textures.rows, (stone_col+1) / Textures.cols,
    stone_row     / Textures.rows, stone_col     / Textures.cols,
    (stone_row+1) / Textures.rows, stone_col     / Textures.cols,
],

Textures.rows and Textures.cols references the number of colums and rows my textures atlas (the file where all the textures are stored in a png grid) has and every block has their own row and col file that references its position. Then, I created a private uv = Array<Array<number>>(); in my Chunk class and modified the terrain generator to push the blocks' uv arrays to it. For example, this is how it is done for positive z faces (note that I have swapped y and z for efficiency purposes):

if (this.blockEmpty(x, z+1, y))
{
    this.terrain.push(Chunk.faces[pz].clone().translate(x, y, z));
    this.uv.push(this.structure[x][z][y].uv_side);
}

Now, BufferGeometry only accepts 'uv' arrays as typed ( Float32Array in this case), so I had to construct one from a flattened version of this.uv . This is how the terrain generator function looks like now:

public generateTerrain (): THREE.Mesh
{
    this.terrain = new Array<THREE.BufferGeometry>();

    for (let x = 0; x < Chunk.base; x++)
        for (let z = 0; z < Chunk.base; z++)
            for (let y = 0; y < Chunk.build_height; y++)
                if (!this.structure[x][z][y].attrs.empty)
                    this.generateBlockFaces(x, z, y);

    const geometry = BufferGeometryUtils.mergeBufferGeometries(this.terrain);
    geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(this.uv.flat()), 2));

    return new THREE.Mesh(geometry, Textures.material);
}

As you can see, the material I'm using comes from the Textures class. Here's the whole imported file:

import * as THREE from 'three';

export default class Textures
{
    private static readonly loader = new THREE.TextureLoader();

    public static readonly rows = 3;
    public static readonly cols = 2;

    public static readonly atlas    = Textures.loader.load("/tex/atlas.png");
    public static readonly material = new THREE.MeshBasicMaterial({map: Textures.atlas});
}

Textures.atlas.magFilter = THREE.NearestFilter;
Textures.atlas.minFilter = THREE.NearestFilter;

And that's it: The terrain now generates rendering every single block's texture and I can't be happier about it :D

For the people who DO want to use a material array:

const materials = [
  new THREE.MeshBasicMaterial( { color: 'red' } ),
  new THREE.MeshBasicMaterial( { color: 'blue' } )
];

const geometries = [
  new THREE.PlaneGeometry( 1, 1 ),
  new THREE.PlaneGeometry( 1, 1 )
];
geometries[ 1 ].rotateX( Math.PI * -0.5 );

// Add groups that set the materialIndex of each vertex in a group
// For this code all the vertices in each geometry have the same material. 
// If you load in a model that already has multiple textures you don't need to do this.
geometries[ 0 ].addGroup( 0, geometries[0].attributes.position.count, 0 ); 
geometries[ 1 ].addGroup( 0, geometries[1].attributes.position.count, 1 );

// Setting true on the second argument enables groups for the merged geometry.
const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries( geometries, true );

const multiMaterialMesh = new THREE.Mesh( mergedGeometry, materials );
scene.add( multiMaterialMesh );

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM