import { GeneratedGeometry } from './generated-geometry.js';
import { GeometryUtils } from './geometry-utils.js';
import { MATH_VECTOR3_INDEXES, MATH_COMMON_VALUES } from '../constants/math.js';
import { ECMASCRIPT_TYPEOF_RESULTS } from '../constants/ecmascript-types.js';
import { SPHERE_DEFAULTS, SPHERE_LIMITS } from '../constants/sphere-geometry.js';
import {
DEFAULT_VERTEX_COLOR,
GEOMETRY_SIZES,
GEOMETRY_GRID,
GEOMETRY_LAYOUT,
GEOMETRY_ANGLES,
GEOMETRY_UV_INDEXES,
GEOMETRY_UV
} from '../constants/geometry.js';
/**
* Sphere geometry options.
*
* `width, height or depth` define full extents (diameters). Use equal values for a perfect sphere.
*
* `colors` supports:
* - Uniform RGB (`length === 3`)
* - Per-vertex RGB (`length === vertexCount * 3`)
*
* Segment parameters must be integers:
* - `widthSegments >= 3`
* - `heightSegments >= 2`
*
* @typedef {Object} SphereGeometryOptions
* @property {number} [width = 1.0] - Diameter along X axis.
* @property {number} [height = 1.0] - Diameter along Y axis.
* @property {number} [depth = 1.0] - Diameter along Z axis.
* @property {number} [widthSegments = 24] - Longitudinal segments.
* @property {number} [heightSegments = 16] - Latitudinal segments.
* @property {Float32Array} [colors] - Color specification buffer.
*/
/**
* Internal geometry buffers produced by `SphereGeometry`.
*
* @typedef {Object} SphereGeometryData
* @property {Float32Array} positions - Vertex positions (xyz).
* @property {Float32Array} normals - Vertex normals (xyz).
* @property {Float32Array} uvs - Vertex UVs (uv).
* @property {Float32Array} colors - Vertex colors (rgb).
* @property {Uint16Array | Uint32Array} indicesSolid - Triangle index buffer.
* @property {Uint16Array | Uint32Array} indicesWireframe - Line index buffer for wireframe.
*/
/**
* Segmented UV sphere geometry. Supports ellipsoid sizes via `width, height or depth`.
*/
export class SphereGeometry extends GeneratedGeometry {
/**
* Normalizes constructor input to a `SphereGeometryOptions` object.
*
* @param {SphereGeometryOptions} options - Options object.
* @returns {Required<SphereGeometryOptions>} - Normalized options.
* @private
*/
static #normalizeOptions(options) {
if (options === null || typeof options !== ECMASCRIPT_TYPEOF_RESULTS.OBJECT) {
throw new TypeError('SphereGeometry expects options as an object.');
}
const {
width = SPHERE_DEFAULTS.WIDTH,
height = SPHERE_DEFAULTS.HEIGHT,
depth = SPHERE_DEFAULTS.DEPTH,
widthSegments = SPHERE_DEFAULTS.WIDTH_SEGMENTS,
heightSegments = SPHERE_DEFAULTS.HEIGHT_SEGMENTS,
colors = DEFAULT_VERTEX_COLOR
} = options;
if (typeof width !== ECMASCRIPT_TYPEOF_RESULTS.NUMBER ||
typeof height !== ECMASCRIPT_TYPEOF_RESULTS.NUMBER ||
typeof depth !== ECMASCRIPT_TYPEOF_RESULTS.NUMBER) {
throw new TypeError('SphereGeometry expects width, height or depth as numbers.');
}
if (!Number.isFinite(width) || !Number.isFinite(height) || !Number.isFinite(depth)) {
throw new RangeError('SphereGeometry expects finite width, height or depth.');
}
if (!(colors instanceof Float32Array)) {
throw new TypeError('SphereGeometry expects colors as a Float32Array.');
}
return {
width,
height,
depth,
widthSegments : GeometryUtils.normalizeSegmentCount(widthSegments, 'widthSegments', SPHERE_LIMITS.MIN_WIDTH_SEGMENT_COUNT, 'SphereGeometry'),
heightSegments : GeometryUtils.normalizeSegmentCount(heightSegments, 'heightSegments', SPHERE_LIMITS.MIN_HEIGHT_SEGMENT_COUNT, 'SphereGeometry'),
colors
};
}
/**
* Generates vertex and index buffers from construction options.
*
* @param {SphereGeometryOptions} [options] - Geometry options.
* @returns {SphereGeometryData} - Generated CPU buffers.
* @protected
*/
static createGeometryData(options = {}) {
const positionComponentCount = GEOMETRY_LAYOUT.POSITION_COMPONENT_COUNT;
const uvComponentCount = GEOMETRY_LAYOUT.UV_COMPONENT_COUNT;
const normalized = SphereGeometry.#normalizeOptions(options);
const widthSegments = normalized.widthSegments;
const heightSegments = normalized.heightSegments;
const widthVertexCount = widthSegments + GEOMETRY_GRID.VERTEX_INCREMENT;
const heightVertexCount = heightSegments + GEOMETRY_GRID.VERTEX_INCREMENT;
const vertexCount = widthVertexCount * heightVertexCount;
const positions = new Float32Array(vertexCount * positionComponentCount);
const normals = new Float32Array(vertexCount * positionComponentCount);
const uvs = new Float32Array(vertexCount * uvComponentCount);
SphereGeometry.#writeVertices(positions, normals, uvs, normalized);
const solidTriangleIndices = [];
GeometryUtils.appendGridTriangleIndices(solidTriangleIndices, widthSegments, heightSegments);
const indicesSolid = GeometryUtils.createIndexArray(vertexCount, solidTriangleIndices);
const indicesWireframe = GeometryUtils.createWireframeIndicesFromSolidIndices(vertexCount, indicesSolid);
const colors = GeometryUtils.createColorsFromSpec(vertexCount, normalized.colors);
return {
positions,
normals,
uvs,
colors,
indicesSolid,
indicesWireframe
};
}
/**
* Computes inverse vector length `(1 / sqrt(x ^ 2 + y ^ 2 + z ^ 2))`.
* Returns 0 when the input vector is zero-length.
*
* @param {number} x - X component.
* @param {number} y - Y component.
* @param {number} z - Z component.
* @returns {number} - Inverse length.
* @private
*/
static #inverseLength(x, y, z) {
const length = Math.sqrt((x * x) + (y * y) + (z * z));
if (length === MATH_COMMON_VALUES.ZERO) {
return MATH_COMMON_VALUES.ZERO;
}
return MATH_COMMON_VALUES.UNIT / length;
}
/**
* Writes the grid positions, normals and texture coordinates.
*
* @param {Float32Array} positions - Output positions.
* @param {Float32Array} normals - Output normals.
* @param {Float32Array} uvs - Output texture coordinates.
* @param {Required<SphereGeometryOptions>} normalized - Normalized geometry options.
* @private
*/
static #writeVertices(positions, normals, uvs, normalized) {
const positionComponentCount = GEOMETRY_LAYOUT.POSITION_COMPONENT_COUNT;
const uvComponentCount = GEOMETRY_LAYOUT.UV_COMPONENT_COUNT;
const xComponentIndex = MATH_VECTOR3_INDEXES.X;
const yComponentIndex = MATH_VECTOR3_INDEXES.Y;
const zComponentIndex = MATH_VECTOR3_INDEXES.Z;
const radiusX = normalized.width / GEOMETRY_SIZES.HALF_SIZE_DIVISOR;
const radiusY = normalized.height / GEOMETRY_SIZES.HALF_SIZE_DIVISOR;
const radiusZ = normalized.depth / GEOMETRY_SIZES.HALF_SIZE_DIVISOR;
const widthSegments = normalized.widthSegments;
const heightSegments = normalized.heightSegments;
const widthVertexCount = widthSegments + GEOMETRY_GRID.VERTEX_INCREMENT;
const heightVertexCount = heightSegments + GEOMETRY_GRID.VERTEX_INCREMENT;
let vertexIndex = MATH_COMMON_VALUES.ZERO;
for (let latitudeIndex = MATH_COMMON_VALUES.ZERO; latitudeIndex < heightVertexCount; latitudeIndex += MATH_COMMON_VALUES.UNIT) {
const vNormalized = latitudeIndex / heightSegments;
const phiRadians = vNormalized * Math.PI;
const sinPhi = Math.sin(phiRadians);
const cosPhi = Math.cos(phiRadians);
for (let longitudeIndex = MATH_COMMON_VALUES.ZERO; longitudeIndex < widthVertexCount; longitudeIndex += MATH_COMMON_VALUES.UNIT) {
const uNormalized = longitudeIndex / widthSegments;
const thetaRadians = uNormalized * GEOMETRY_ANGLES.FULL_TURN;
const sinTheta = Math.sin(thetaRadians);
const cosTheta = Math.cos(thetaRadians);
const positionX = cosTheta * sinPhi * radiusX;
const positionY = cosPhi * radiusY;
const positionZ = sinTheta * sinPhi * radiusZ;
const positionBaseOffset = vertexIndex * positionComponentCount;
positions[positionBaseOffset + xComponentIndex] = positionX;
positions[positionBaseOffset + yComponentIndex] = positionY;
positions[positionBaseOffset + zComponentIndex] = positionZ;
const normalX0 = (radiusX !== MATH_COMMON_VALUES.ZERO) ? (positionX / (radiusX * radiusX)) : MATH_COMMON_VALUES.ZERO;
const normalY0 = (radiusY !== MATH_COMMON_VALUES.ZERO) ? (positionY / (radiusY * radiusY)) : MATH_COMMON_VALUES.ZERO;
const normalZ0 = (radiusZ !== MATH_COMMON_VALUES.ZERO) ? (positionZ / (radiusZ * radiusZ)) : MATH_COMMON_VALUES.ZERO;
const inverseNormalLength = SphereGeometry.#inverseLength(normalX0, normalY0, normalZ0);
normals[positionBaseOffset + xComponentIndex] = normalX0 * inverseNormalLength;
normals[positionBaseOffset + yComponentIndex] = normalY0 * inverseNormalLength;
normals[positionBaseOffset + zComponentIndex] = normalZ0 * inverseNormalLength;
const uvBaseOffset = vertexIndex * uvComponentCount;
uvs[uvBaseOffset + GEOMETRY_UV_INDEXES.U] = uNormalized;
uvs[uvBaseOffset + GEOMETRY_UV_INDEXES.V] = GEOMETRY_UV.V_FLIP_BASE - vNormalized;
vertexIndex += MATH_COMMON_VALUES.UNIT;
}
}
}
}