Three.js: 无法显示使用纹理数组创建的网格
Three.js: Cannot display mesh created with texture array
我正在构建一个非常原始的游戏,基于你放置在沙盒世界中的立方体(一个完全独特的概念,它将彻底改变我们所知道的游戏),我正在研究块生成。这是我目前所拥有的:
我的块是在对象字面量中定义的:
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")}),
},
};
这是我的 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
);
}
}
我知道网格创建器应该与模型分离,但现在我正在试验。 class 是这样工作的:
首先,constructor()
创建一个 Block
的 3D 矩阵。我已将其设置为以 air
和 grass
的棋盘模式创建它,因此每隔一个块都是空的。
接下来,我从场景中调用 generateTerrain()
:
this.chunk = new Chunk();
this.add(this.chunk.generateTerrain());
调用此方法时,它为每个非空块输入 generateBlockFaces
并将适当的 PlaneGeometry
推入 terrain
数组以及适当的 THREE.MeshBasicMaterial
到 materials
数组中。然后我使用 BufferGeometryUtils.mergeBufferGeometries
合并几何图形并创建传递合并几何图形和 materials
数组的网格。
我遇到的问题是,在传递 new THREE.MeshNormalMaterial
或任何其他 material 时创建网格效果非常好,但在传递 materials
数组时则不然。传递数组会创建对象(并且 console.log
ing 它表明它是在没有错误的情况下创建的),但它不是用场景绘制的。
我是否误以为 materials
数组会为每个面分配一个 material?我做错了什么?
我在文档中找到对 THREE.UVMapping 的引用后解决了它。将几何图形发送到 GPU 时,纹理坐标需要是顶点坐标的双面坐标。为此,我在块中定义了以下三个属性:
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
和 Textures.cols
引用我的纹理图集(所有纹理都存储在 png 网格中的文件)的列数和行数,每个块都有自己的 row
和 col
引用其位置的文件。然后,我在 Chunk
class 中创建了一个 private uv = Array<Array<number>>();
并修改了地形生成器以将块的 uv 数组推送到它。例如,这是对正 z 面的处理方式(请注意,为了提高效率,我交换了 y
和 z
):
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);
}
现在,BufferGeometry
只接受类型化的 'uv'
数组(在本例中为 Float32Array
),因此我不得不从 this.uv
的扁平化版本构建一个数组。这就是地形生成器函数现在的样子:
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);
}
如您所见,我使用的 material 来自 Textures
class。这是整个导入的文件:
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;
就是这样!地形现在生成渲染每个块的纹理,我对此感到非常高兴:D
对于确实想要使用 material 数组的人:
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 );
我正在构建一个非常原始的游戏,基于你放置在沙盒世界中的立方体(一个完全独特的概念,它将彻底改变我们所知道的游戏),我正在研究块生成。这是我目前所拥有的:
我的块是在对象字面量中定义的:
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")}),
},
};
这是我的 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
);
}
}
我知道网格创建器应该与模型分离,但现在我正在试验。 class 是这样工作的:
首先,constructor()
创建一个 Block
的 3D 矩阵。我已将其设置为以 air
和 grass
的棋盘模式创建它,因此每隔一个块都是空的。
接下来,我从场景中调用 generateTerrain()
:
this.chunk = new Chunk();
this.add(this.chunk.generateTerrain());
调用此方法时,它为每个非空块输入 generateBlockFaces
并将适当的 PlaneGeometry
推入 terrain
数组以及适当的 THREE.MeshBasicMaterial
到 materials
数组中。然后我使用 BufferGeometryUtils.mergeBufferGeometries
合并几何图形并创建传递合并几何图形和 materials
数组的网格。
我遇到的问题是,在传递 new THREE.MeshNormalMaterial
或任何其他 material 时创建网格效果非常好,但在传递 materials
数组时则不然。传递数组会创建对象(并且 console.log
ing 它表明它是在没有错误的情况下创建的),但它不是用场景绘制的。
我是否误以为 materials
数组会为每个面分配一个 material?我做错了什么?
我在文档中找到对 THREE.UVMapping 的引用后解决了它。将几何图形发送到 GPU 时,纹理坐标需要是顶点坐标的双面坐标。为此,我在块中定义了以下三个属性:
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
和 Textures.cols
引用我的纹理图集(所有纹理都存储在 png 网格中的文件)的列数和行数,每个块都有自己的 row
和 col
引用其位置的文件。然后,我在 Chunk
class 中创建了一个 private uv = Array<Array<number>>();
并修改了地形生成器以将块的 uv 数组推送到它。例如,这是对正 z 面的处理方式(请注意,为了提高效率,我交换了 y
和 z
):
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);
}
现在,BufferGeometry
只接受类型化的 'uv'
数组(在本例中为 Float32Array
),因此我不得不从 this.uv
的扁平化版本构建一个数组。这就是地形生成器函数现在的样子:
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);
}
如您所见,我使用的 material 来自 Textures
class。这是整个导入的文件:
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;
就是这样!地形现在生成渲染每个块的纹理,我对此感到非常高兴:D
对于确实想要使用 material 数组的人:
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 );