Issue in Three.js: Unable to render mesh generated with an array of textures

In the process of developing an innovative game centered around cubes in a virtual sandbox environment (a groundbreaking concept poised to revolutionize the gaming industry), I am currently focusing on chunk generation. This is a glimpse into my progress thus far:

The blocks are defined within 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")}),
    },
};

Introducing 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 acknowledge the need to separate the mesh creation logic from the model, but currently, I am exploring different approaches. The functionality of the class operates as follows:

Initially, the constructor() method establishes a 3D matrix of Block entities. It is configured to generate these entities in a checkerboard pattern of air and grass, resulting in every other block being empty.

Subsequently, I invoke generateTerrain() within my Scene:

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

Upon calling this method, it iterates through each non-empty block, invoking generateBlockFaces to populate the appropriate PlaneGeometrys in the terrain array along with their respective THREE.MeshBasicMaterial counterparts in the materials array. The geometries are merged using

BufferGeometryUtils.mergeBufferGeometries</code, creating a mesh by combining the merged geometry with the <code>materials
array.

Experiencing difficulties arise when attempting to create the mesh using the materials array, unlike with new THREE.MeshNormalMaterial or any alternative material parameters which work seamlessly. While the object is successfully constructed when passing the materials array (confirmed via console.log outputs without errors), it fails to display within the scene.

Is my assumption that the materials array assigns a material to each face incorrect? What could be the root cause of this issue?

Answer №1

After some research, I came across a reference to THREE.UVMapping in the documentation which helped me solve the issue at hand. It was vital to ensure that when sending geometry to the GPU, texture coordinates are a bijection from the vertices coordinates. To achieve this, I defined three attributes in my blocks as follows:

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,
],

The references Textures.rows and Textures.cols indicate the number of columns and rows in my textures atlas (where all textures are stored in a png grid). Each block has its unique row and col values to determine its position. I created a private array called

uv = Array<Array<number>>
in my Chunk class and adjusted the terrain generator to populate the UV arrays for each block. For instance, here's how it's done for positive z faces:

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 data (in this case, Float32Array). Therefore, I had to construct one from a flattened version of this.uv. This is how the updated terrain generator function looks like:

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);
}

It's worth mentioning that the material used is sourced from the Textures class. Here's the content of the 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;

That sums it up! The terrain now generates with every single block's texture rendering correctly, and I couldn't be more pleased about the outcome :D

Answer №2

For those interested in utilizing an array of materials:

const materialArray = [
  new THREE.MeshPhongMaterial( { color: 'green' } ),
  new THREE.MeshPhongMaterial( { color: 'yellow' } )
];

const shapes = [
  new THREE.BoxGeometry( 1, 1, 1 ),
  new THREE.SphereGeometry( 0.5, 20, 20 )
];
shapes[ 1 ].rotateX( Math.PI * -0.3 );

// Define groups for assigning material indices to vertices
// Each geometry will have its own set of materials applied
shapes[ 0 ].addGroup( 0, shapes[0].attributes.position.count, 0 ); 
shapes[ 1 ].addGroup( 0, shapes[1].attributes.position.count, 1 );

// Use the second argument as true to enable grouping in merged geometry.
const combinedGeometry = BufferGeometryUtils.mergeBufferGeometries( shapes, true );

const multiMaterialObject = new THREE.Mesh( combinedGeometry, materialArray );
scene.add( multiMaterialObject );

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Exploring the world of BDD with Node.js and Angular in an actual web

Currently, I am in the midst of developing an application using nodeJs + yo angular-fullstck, starting with BDD and TDD practices. After searching through multiple resources, I have decided to utilize cucumber-js and Jasmin with the karma runner for testi ...

Sending parameters to async.parallel in Node.js

My goal is to create a simple example that demonstrates the concept I have described. Below is my attempt at a minimal example, where I expect to see the following output: negative of 1 is -1 plus one of 2 is 3 Here is the code I've written: var asy ...

What steps can I take to prevent my JavaScript code from interfering with my pre-established CSS styles?

I'm currently designing a mini-email platform that serves as a prototype rather than a fully functional application. The HTML file contains placeholder emails that have been styled to appear presentable. I've implemented an input bar and used Jav ...

The sum is being treated as a concatenation instead of an addition in this case

Why is the somma value showing the concatenation of totaleEnergetico and totaleStrutturale instead of a sum? RiepilogoCombinatoStComponent.ts export class RiepilogoCombinatoStComponent implements OnInit { constructor() { } interventi: AssociazioneI ...

disable parent directive execution when child element is clicked

Here I have implemented a pop-over directive, and I want to apply it to both the parent and child elements. When I click on the child element, the parent directive is also displayed. How can I prevent event propagation to the parent element? Is it possible ...

Attempting to showcase a button element within a popover, although it is failing to appear

I have implemented a feature where a popover appears when hovering over an inbox icon (font-awesome). However, I am facing an issue with displaying a button on the second row within the popover. The title is showing up fine, but the button is not appearing ...

Invoking function to utilize xmlhttprequest open() and send()

I have encountered an issue while attempting to send a GET request from a form using the form's onsubmit='function();' and AJAX. <html> <head> <script type='text/javascript'> var xmlhttp = new XMLHttpRequest( ...

Disappear solely upon clicking on the menu

Currently, I am working on implementing navigation for menu items. The functionality I want to achieve is that when a user hovers over a menu item, it extends, and when they move the mouse away, it retracts. I have been able to make the menu stay in the ex ...

Pressing the cancel button triggers a refresh of the page within the iframe

I currently have an iframe embedded in my website, and within it is the following jQuery code: $(document).ready(function() { $('#print_button').click(function() { window.print(); }); }); The issue at hand is that when I click ...

Variations in jQuery's append method when dealing with a string, a reference to a jQuery object

Here are multiple ways to add a div element to the body of an HTML document. But what distinctions exist between them and in what scenarios should each be utilized for optimal performance? var newDiv = '<div id="divid"></div>'; $(&ap ...

Guide to making a drawer with headlessui/vue and tailwindcss!

Recently, I decided to create a drawer component using headlessui/vue and tailwindcss. While browsing the headless ui documentation for vuejs, I could not find any examples of a drawer component. So, I took it upon myself to create one using the headlessu ...

I'm encountering an issue with the React state where it seems to be endlessly nesting

I am new to React and I seem to be encountering an issue where every time I trigger a click event, the state object continues to nest. The code snippet below is part of the App component. const [state, setstate] = useState('') useEffect(() =& ...

Revitalizing and rerouting page upon button click

The issue at hand is: When the "Post now" button is clicked, the modal with the filled form still appears. If the button is clicked again, it adds the same data repeatedly. I aim to have the page refresh and navigate to a link containing the prediction d ...

What is the best way to locate the closest element using JavaScript?

Is there a way to locate the closest object to the mouse pointer on a webpage? I have a hypothesis that involves utilizing the array function, however, I am uncertain if that is the correct approach. Furthermore, I lack knowledge of which specific proper ...

Using scrollIntoView() in combination with Angular Material's Mat-Menu-Item does not produce the desired result

I am facing an issue with angular material and scrollIntoView({ behavior: 'smooth', block: 'start' }). My goal is to click on a mat-menu-item, which is inside an item in a mat-table, and scroll to a specific HTML tag This is my target ...

Retrieving JSON data via an AJAX call

Similar Question: Sending PHP json_encode array to jQuery I've created a function that searches a database for a specific name using $.post. It returns user details with matching search criteria in JSON format generated by PHP, like this: Arra ...

Reading properties of undefined in React is not possible. The log method only functions on objects

I'm currently facing an issue while developing a weather website using the weatherapi. When I try to access properties deeper than the initial object of location, like the city name, it throws an error saying "cannot read properties of undefined." Int ...

Combining several arrays to find the array of shared elements as the final result

In my JavaScript/TypeScript application built with Angular, I am facing the challenge of merging multiple arrays and extracting the common values that appear in all arrays. For example: arrayOne = [34, 23, 80, 93, 48] arrayTwo = [48, 29, 10, 79, 23] arrayT ...

Searching for a specific element within a JSON object by iterating through it with JavaScript

Here is the JSON file I am working with: { "src": "Dvc", "rte": { "src": "dvc" }, "msgTyp": "dvc.act", "srcTyp": "dvc.act", "cntxt": "", "obj": { "clbcks": [ { "type": "", "title": "", "fields": [ { "src": "label.PNG", ...