import { MathUtils } from '../math/MathUtils';
/**
* @class MeshBuilder
* The mesh builder.
* @memberof THING
*/
class MeshBuilder {
/**
* @typedef {object} MeshResult
* @property {Array<number>} position The position.
* @property {Array<number>} normal The normal.
* @property {Array<number>} uv The uv.
* @property {Array<number>} index The index of position.
*/
static createPlane(width = 1, height = 1, widthSegments = 1, heightSegments = 1) {
const plane = {};
plane.position = [];
plane.normal = [];
plane.uv = [];
plane.index = [];
const segmentWidth = width / widthSegments;
const segmentHeight = height / heightSegments;
for (let j = 0; j <= heightSegments; j++) {
const yPos = j * segmentHeight - height / 2;
for (let i = 0; i <= widthSegments; i++) {
const xPos = i * segmentWidth - width / 2;
plane.position.push(xPos, yPos, 0);
plane.normal.push(0, 0, 1);
plane.uv.push(i / widthSegments, j / heightSegments);
}
}
for (let j = 0; j < heightSegments; j++) {
for (let i = 0; i < widthSegments; i++) {
const a = i + (widthSegments + 1) * j;
const b = i + (widthSegments + 1) * (j + 1);
const c = a + 1;
const d = b + 1;
plane.index.push(a, b, c);
plane.index.push(b, d, c);
}
}
return plane;
}
/**
* @public
* @typedef {object} CircleParam 构造圆形需要的参数
* @property {number} [radius=1] 半径
* @property {number} [segments=64] 模拟圆形使用的多边形的边数
* @property {number} [startRad=0] 开始位置(弧度)
* @property {number} [arc=Math.PI * 2] 所形成的扇形的角度(弧度)
* @example
* let cirecle = THING.MeshBuilder.createCircle();
* // @expect(cirecle.index.length == 192 );
*/
static createCircle({ radius = 1, segments = 64, startRad = 0, arc = MathUtils.PI * 2 } = {}) {
let circle = {};
circle.position = [];
circle.normal = [];
circle.uv = [];
circle.index = [];
circle.position.push(0, 0, 0);
circle.normal.push(0, 0, 1);
circle.uv.push(0.5, 0.5);
for (let s = 0, i = 3; s <= segments; s++, i += 3) {
const segment = startRad + s / segments * arc;
circle.position.push(radius * MathUtils.cos(segment), radius * MathUtils.sin(segment), 0);
circle.normal.push(0, 0, 1);
circle.uv.push((circle.position[i] / radius + 1) / 2, (circle.position[i + 1] / radius + 1) / 2);
}
for (let i = 1; i <= segments; i++) {
circle.index.push(i, i + 1, 0);
}
return circle;
}
/**
* @public
* @typedef {object} CylinderParam 构造圆柱需要的参数
* @property {number} [radiusTop=1] 顶部半径
* @property {number} [radiusBottom=1] 底部半径
* @property {number} [height=2] 高度
* @property {number} [radialSegments=64] 圆柱体截面是几边形 默认64代表圆柱体截面是64边形
* @property {number} [heightSegments=64] 高度方向分段数
* @property {boolean} [openEnded=false] 顶面和地面是否封闭
* @property {number} [thetaStart=0] 开始的角度(弧度)
* @property {number} [thetaLength=Math.PI*2] 结束角度(弧度)
*/
static createCylinder({ radiusTop = 1, radiusBottom = 1, height = 2, radialSegments = 64, heightSegments = 1, openEnded = false, thetaStart = 0, thetaLength = MathUtils.PI * 2, poslength = 0 } = {}) {
let cylinder = {};
cylinder.position = [];
cylinder.normal = [];
cylinder.uv = [];
cylinder.index = [];
radialSegments = MathUtils.floor(radialSegments);
heightSegments = MathUtils.floor(heightSegments);
let indexTemp = 0;
const indexArray = [];
const halfHeight = height / 2;
generateTorso();
if (openEnded === false) {
if (radiusTop > 0) generateCap(true);
if (radiusBottom > 0) generateCap(false);
}
function generateTorso() {
const slope = (radiusBottom - radiusTop) / height;
for (let y = 0; y <= heightSegments; y++) {
const indexRow = [];
const v = y / heightSegments;
const radius = v * (radiusBottom - radiusTop) + radiusTop;
for (let x = 0; x <= radialSegments; x++) {
const u = x / radialSegments;
const theta = u * thetaLength + thetaStart;
const sinTheta = MathUtils.sin(theta);
const cosTheta = MathUtils.cos(theta);
const normalNor = MathUtils.normalizeVector([sinTheta, slope, cosTheta]);
cylinder.position.push(radius * sinTheta, -v * height + halfHeight, radius * cosTheta);
cylinder.normal.push(normalNor[0], normalNor[1], normalNor[2]);
cylinder.uv.push(u, 1 - v);
indexRow.push(indexTemp++);
}
indexArray.push(indexRow);
}
for (let x = 0; x < radialSegments; x++) {
for (let y = 0; y < heightSegments; y++) {
const a = indexArray[y][x];
const b = indexArray[y + 1][x];
const c = indexArray[y + 1][x + 1];
const d = indexArray[y][x + 1];
cylinder.index.push(a + poslength, b + poslength, d + poslength);
cylinder.index.push(b + poslength, c + poslength, d + poslength);
}
}
}
function generateCap(top) {
const centerIndexStart = indexTemp;
const radius = (top === true) ? radiusTop : radiusBottom;
const sign = (top === true) ? 1 : -1;
for (let x = 1; x <= radialSegments; x++) {
cylinder.position.push(0, halfHeight * sign, 0);
cylinder.normal.push(0, sign, 0);
cylinder.uv.push(0.5, 0.5);
indexTemp++;
}
const centerIndexEnd = indexTemp;
for (let x = 0; x <= radialSegments; x++) {
const u = x / radialSegments;
const theta = u * thetaLength + thetaStart;
const cosTheta = MathUtils.cos(theta);
const sinTheta = MathUtils.sin(theta);
cylinder.position.push(radius * sinTheta, halfHeight * sign, radius * cosTheta);
cylinder.normal.push(0, sign, 0);
cylinder.uv.push((sinTheta * 0.5) + 0.5, -(cosTheta * 0.5 * sign) + 0.5);
indexTemp++;
}
for (let x = 0; x < radialSegments; x++) {
const c = centerIndexStart + x;
const i = centerIndexEnd + x;
if (top === true) {
cylinder.index.push(i + poslength, i + 1 + poslength, c + poslength);
}
else {
cylinder.index.push(i + 1 + poslength, i + poslength, c + poslength);
}
}
}
return cylinder;
}
/**
* @public
* @typedef {object} TorusParam 构造圆环需要的参数
* @property {number} [radius=0.8] 圆环内径
* @property {number} [tube=0.2] 圆环截面(管)的半径
* @property {number} [radialSegments=64] 圆环横切面使用多少边形
* @property {number} [tubularSegments=64] 圆环纵切面(管)使用多少边形
* @property {number} [arc=Math.PI*2] 圆环范围(默认是一圈,即Math.PI*2,如果是Math.PI,代表圆环是一个半圆)
*/
static createTorus({ radius = 0.8, tube = 0.2, radialSegments = 64, tubularSegments = 64, arc = MathUtils.PI * 2, startRad = 0 } = {}) {
let torus = {};
torus.position = [];
torus.normal = [];
torus.uv = [];
torus.index = [];
radialSegments = MathUtils.floor(radialSegments);
tubularSegments = MathUtils.floor(tubularSegments);
let centerTemp = [];
let vertexTemp = [];
let normalTemp = [];
for (let j = 0; j <= radialSegments; j++) {
for (let i = 0; i <= tubularSegments; i++) {
const u = i / tubularSegments * arc + startRad;
const v = j / radialSegments * MathUtils.PI * 2;
vertexTemp = [
(radius + tube * MathUtils.cos(v)) * MathUtils.cos(u),
(radius + tube * MathUtils.cos(v)) * MathUtils.sin(u),
tube * MathUtils.sin(v)
];
torus.position.push(
(radius + tube * MathUtils.cos(v)) * MathUtils.cos(u),
(radius + tube * MathUtils.cos(v)) * MathUtils.sin(u),
tube * MathUtils.sin(v)
);
centerTemp = [radius * MathUtils.cos(u), radius * MathUtils.sin(u), 0];
normalTemp = MathUtils.normalizeVector(MathUtils.subVector(vertexTemp, centerTemp));
torus.normal.push(normalTemp[0], normalTemp[1], normalTemp[2]);
torus.uv.push(i / tubularSegments);
torus.uv.push(j / radialSegments);
}
}
for (let j = 1; j <= radialSegments; j++) {
for (let i = 1; i <= tubularSegments; i++) {
const a = (tubularSegments + 1) * j + i - 1;
const b = (tubularSegments + 1) * (j - 1) + i - 1;
const c = (tubularSegments + 1) * (j - 1) + i;
const d = (tubularSegments + 1) * j + i;
torus.index.push(a, b, d);
torus.index.push(b, c, d);
}
}
return torus;
}
/**
* @typedef {object} CapsuleParam 构造胶囊需要的参数
* @property {number} [radius=0.5] 中间界面的半径
* @property {number} [cylinderHeight=1.0] 胶囊的高度
* @property {number} [widthSegments=64] 横截面是多少边形,值越大越精细,三角面越密集
* @property {number} [heightSegments=64] 高度方向片段个数,值越大越精细,三角面越密集
* @public
*/
static createCapsule({ radius = 0.5, cylinderHeight = 1, widthSegments = 64, heightSegments = 64 } = {}) {
let capsule = {};
capsule.position = [];
capsule.normal = [];
capsule.uv = [];
capsule.index = [];
let upSphere = mathSphere(radius, widthSegments, heightSegments, 0, -MathUtils.PI * 2, -MathUtils.PI / 2, MathUtils.PI, cylinderHeight / 2);
let downSphere = mathSphere(radius, widthSegments, heightSegments, 0, -MathUtils.PI * 2, MathUtils.PI / 2, MathUtils.PI, -cylinderHeight / 2, upSphere.position.length / 3);
let cylinder = this.createCylinder({
radiusTop: radius, radiusBottom: radius, height: cylinderHeight, radialSegments: heightSegments, heightSegments: 1,
openEnded: true, thetaStart: -MathUtils.PI / 4 * 2, thetaLength: MathUtils.PI * 2, poslength: upSphere.position.length / 3 + downSphere.position.length / 3
})
capsule.position = [
...upSphere.position,
...downSphere.position,
...cylinder.position
];
capsule.normal = [
...upSphere.normal,
...downSphere.normal,
...cylinder.normal
];
capsule.uv = [
...upSphere.uv,
...downSphere.uv,
...cylinder.uv
];
capsule.index = [
...upSphere.index,
...downSphere.index,
...cylinder.index
];
function mathSphere(radius = 10, widthSegments = 8, heightSegments = 8, phiStart = 0, phiLength = MathUtils.PI * 2,
thetaStart = MathUtils.PI, thetaLength = MathUtils.PI, heightOffset = 0, poslength = 0) {
let sphere = {};
sphere.position = [];
sphere.normal = [];
sphere.uv = [];
sphere.index = [];
widthSegments = MathUtils.max(3, MathUtils.floor(widthSegments));
heightSegments = MathUtils.max(2, MathUtils.floor(heightSegments));
const thetaEnd = MathUtils.min(thetaStart + thetaLength, MathUtils.PI);
let indexTemp = 0;
const grid = [];
for (let iy = 0; iy <= heightSegments; iy++) {
const verticesRow = [];
const v = iy / heightSegments;
let uOffset = 0;
if (iy == 0 && thetaStart == 0) {
uOffset = 0.5 / widthSegments;
}
else if (iy == heightSegments && thetaEnd == MathUtils.PI) {
uOffset = -0.5 / widthSegments;
}
for (let ix = 0; ix <= widthSegments; ix++) {
const u = ix / widthSegments;
sphere.position.push(
-radius * MathUtils.cos(phiStart + u * phiLength) * MathUtils.sin(thetaStart + v * thetaLength),
radius * MathUtils.cos(thetaStart + v * thetaLength) + heightOffset,
radius * MathUtils.sin(phiStart + u * phiLength) * MathUtils.sin(thetaStart + v * thetaLength)
);
let normali = [
-radius * MathUtils.cos(phiStart + u * phiLength) * MathUtils.sin(thetaStart + v * thetaLength),
radius * MathUtils.cos(thetaStart + v * thetaLength),
radius * MathUtils.sin(phiStart + u * phiLength) * MathUtils.sin(thetaStart + v * thetaLength)
]
normali = MathUtils.normalizeVector(normali);
sphere.normal.push(normali[0], normali[1], normali[2]);
if (thetaStart > 0) { sphere.uv.push(-u, -v); }
else { sphere.uv.push(-u, v); }
verticesRow.push(indexTemp++);
}
grid.push(verticesRow);
}
for (let iy = 0; iy < heightSegments / 2; iy++) {
for (let ix = 0; ix < widthSegments; ix++) {
const a = grid[iy][ix + 1];
const b = grid[iy][ix];
const c = grid[iy + 1][ix];
const d = grid[iy + 1][ix + 1];
if (thetaStart > 0) {
sphere.index.push(a + poslength, d + poslength, b + poslength);
sphere.index.push(b + poslength, d + poslength, c + poslength);
}
else {
sphere.index.push(b + poslength, c + poslength, d + poslength);
sphere.index.push(b + poslength, d + poslength, a + poslength);
}
}
}
return sphere;
}
return capsule;
}
static createSphere({ radius = 1, widthSegments = 32, heightSegments = 16, phiStart = 0, phiLength = MathUtils.PI * 2,
thetaStart = 0, thetaLength = MathUtils.PI } = {}) {
let sphere = {};
sphere.position = [];
sphere.normal = [];
sphere.uv = [];
sphere.index = [];
widthSegments = MathUtils.max(3, MathUtils.floor(widthSegments));
heightSegments = MathUtils.max(2, MathUtils.floor(heightSegments));
const thetaEnd = MathUtils.min(thetaStart + thetaLength, MathUtils.PI);
let indexTemp = 0;
const grid = [];
for (let iy = 0; iy <= heightSegments; iy++) {
const verticesRow = [];
const v = iy / heightSegments;
let uOffset = 0;
if (iy == 0 && thetaStart == 0) {
uOffset = 0.5 / widthSegments;
}
else if (iy == heightSegments && thetaEnd == MathUtils.PI) {
uOffset = -0.5 / widthSegments;
}
for (let ix = 0; ix <= widthSegments; ix++) {
const u = ix / widthSegments;
let vertex = [
-radius * MathUtils.cos(phiStart + u * phiLength) * MathUtils.sin(thetaStart + v * thetaLength),
radius * MathUtils.cos(thetaStart + v * thetaLength),
radius * MathUtils.sin(phiStart + u * phiLength) * MathUtils.sin(thetaStart + v * thetaLength)
];
sphere.position.push(...vertex);
let normali = MathUtils.normalizeVector(vertex);
sphere.normal.push(...normali);
sphere.uv.push(u + uOffset, 1 - v);
verticesRow.push(indexTemp++);
}
grid.push(verticesRow);
}
for (let iy = 0; iy < heightSegments; iy++) {
for (let ix = 0; ix < widthSegments; ix++) {
const a = grid[iy][ix + 1];
const b = grid[iy][ix];
const c = grid[iy + 1][ix];
const d = grid[iy + 1][ix + 1];
if (iy !== 0 || thetaStart > 0) sphere.index.push(a, b, d);
if (iy !== heightSegments - 1 || thetaEnd < Math.PI) sphere.index.push(b, c, d);
}
}
return sphere;
}
static createRing({ innerRadius = 0.5, outerRadius = 1, thetaSegments = 32, phiSegments = 1, thetaStart = 0, thetaLength = Math.PI * 2 } = {}) {
let ring = {};
ring.position = [];
ring.normal = [];
ring.uv = [];
ring.index = [];
thetaSegments = MathUtils.max(3, MathUtils.floor(thetaSegments));
phiSegments = MathUtils.max(2, MathUtils.floor(phiSegments));
let radius = innerRadius;
const radiusStep = ((outerRadius - innerRadius) / phiSegments);
for (let j = 0; j <= phiSegments; j++) {
for (let i = 0; i <= thetaSegments; i++) {
// values are generate from the inside of the ring to the outside
const segment = thetaStart + i / thetaSegments * thetaLength;
// vertex
const vertex = [
radius * MathUtils.cos(segment),
radius * MathUtils.sin(segment),
0
];
ring.position.push(...vertex);
// normal
ring.normal.push(0, 0, 1);
// uv
const uv = [
(vertex[0] / outerRadius + 1) / 2,
(vertex[1] / outerRadius + 1) / 2
];
ring.uv.push(...uv);
}
// increase the radius for next row of vertices
radius += radiusStep;
}
// indices
for (let j = 0; j < phiSegments; j++) {
const thetaSegmentLevel = j * (thetaSegments + 1);
for (let i = 0; i < thetaSegments; i++) {
const segment = i + thetaSegmentLevel;
const a = segment;
const b = segment + thetaSegments + 1;
const c = segment + thetaSegments + 2;
const d = segment + 1;
// faces
ring.index.push(a, b, d);
ring.index.push(b, c, d);
}
}
return ring;
}
static createBox({ width = 1, height = 1, depth = 1, widthSegments = 1, heightSegments = 1, depthSegments = 1 } = {}) {
// segments
widthSegments = Math.floor(widthSegments);
heightSegments = Math.floor(heightSegments);
depthSegments = Math.floor(depthSegments);
let box = {};
box.position = [];
box.normal = [];
box.uv = [];
box.index = [];
// helper variables
let numberOfVertices = 0;
let groupStart = 0;
buildPlane('z', 'y', 'x', -1, -1, depth, height, width, depthSegments, heightSegments, 0); // px
buildPlane('z', 'y', 'x', 1, -1, depth, height, -width, depthSegments, heightSegments, 1); // nx
buildPlane('x', 'z', 'y', 1, 1, width, depth, height, widthSegments, depthSegments, 2); // py
buildPlane('x', 'z', 'y', 1, -1, width, depth, -height, widthSegments, depthSegments, 3); // ny
buildPlane('x', 'y', 'z', 1, -1, width, height, depth, widthSegments, heightSegments, 4); // pz
buildPlane('x', 'y', 'z', -1, -1, width, height, -depth, widthSegments, heightSegments, 5); // nz
function buildPlane(u, v, w, udir, vdir, width, height, depth, gridX, gridY, materialIndex) {
const segmentWidth = width / gridX;
const segmentHeight = height / gridY;
const widthHalf = width / 2;
const heightHalf = height / 2;
const depthHalf = depth / 2;
const gridX1 = gridX + 1;
const gridY1 = gridY + 1;
let vertexCounter = 0;
let groupCount = 0;
const vector = { x: 0, y: 0, z: 0 };
// generate vertices, normals and uvs
for (let iy = 0; iy < gridY1; iy++) {
const y = iy * segmentHeight - heightHalf;
for (let ix = 0; ix < gridX1; ix++) {
const x = ix * segmentWidth - widthHalf;
// set values to correct vector component
vector[u] = x * udir;
vector[v] = y * vdir;
vector[w] = depthHalf;
// now apply vector to vertex buffer
box.position.push(vector.x, vector.y, vector.z);
// set values to correct vector component
vector[u] = 0;
vector[v] = 0;
vector[w] = depth > 0 ? 1 : -1;
// now apply vector to normal buffer
box.normal.push(vector.x, vector.y, vector.z);
// uvs
box.uv.push(ix / gridX);
box.uv.push(1 - (iy / gridY));
// counters
vertexCounter += 1;
}
}
// indices
// 1. you need three indices to draw a single face
// 2. a single segment consists of two faces
// 3. so we need to generate six (2*3) indices per segment
for (let iy = 0; iy < gridY; iy++) {
for (let ix = 0; ix < gridX; ix++) {
const a = numberOfVertices + ix + gridX1 * iy;
const b = numberOfVertices + ix + gridX1 * (iy + 1);
const c = numberOfVertices + (ix + 1) + gridX1 * (iy + 1);
const d = numberOfVertices + (ix + 1) + gridX1 * iy;
// faces
box.index.push(a, b, d);
box.index.push(b, c, d);
// increase counter
groupCount += 6;
}
}
// calculate new start value for groups
groupStart += groupCount;
// update total number of vertices
numberOfVertices += vertexCounter;
}
return box;
}
}
export { MeshBuilder };