@kitware/vtk.js
Version:
Visualization Toolkit for the Web
512 lines (483 loc) • 17.7 kB
JavaScript
import { m as macro } from '../../../macros2.js';
import { B as degreesFromRadians } from '../../../Common/Core/Math/index.js';
import vtkActor from '../../../Rendering/Core/Actor.js';
import vtkCamera from '../../../Rendering/Core/Camera.js';
import vtkDataArray from '../../../Common/Core/DataArray.js';
import vtkPolyData from '../../../Common/DataModel/PolyData.js';
import vtkMapper from '../../../Rendering/Core/Mapper.js';
import vtkCellArray from '../../../Common/Core/CellArray.js';
import vtkTransform from '../../../Common/Transform/Transform.js';
import GLTFParser from './Parser.js';
import { ALPHA_MODE, SEMANTIC_ATTRIBUTE_MAP, MODES } from './Constants.js';
import { loadImage, createVTKTextureFromGLTFTexture } from './Utils.js';
import { handleKHRMaterialsSpecular, handleKHRMaterialsIor, handleKHRMaterialsUnlit, handleKHRMaterialsVariants, handleKHRLightsPunctual, handleKHRDracoMeshCompression } from './Extensions.js';
import { mat4, vec3, quat } from 'gl-matrix';
const {
vtkWarningMacro,
vtkDebugMacro
} = macro;
/**
* Parses a GLTF objects
* @param {Object} gltf - The GLTF object to parse
* @returns {glTF} The parsed GLTF object
*/
async function parseGLTF(gltf, options) {
const parser = new GLTFParser(gltf, options);
const tree = await parser.parse();
return tree;
}
/**
* Creates VTK polydata from a GLTF mesh primitive
* @param {GLTFPrimitive} primitive - The GLTF mesh primitive
* @returns {vtkPolyData} The created VTK polydata
*/
async function createPolyDataFromGLTFMesh(primitive) {
if (!primitive || !primitive.attributes) {
vtkWarningMacro('Primitive has no position data, skipping');
return null;
}
if (primitive.extensions?.KHR_draco_mesh_compression) {
return handleKHRDracoMeshCompression(primitive.extensions.KHR_draco_mesh_compression);
}
const polyData = vtkPolyData.newInstance();
const cells = vtkCellArray.newInstance();
const pointData = polyData.getPointData();
const attrs = Object.entries(primitive.attributes);
attrs.forEach(async ([attributeName, accessor]) => {
switch (attributeName) {
case SEMANTIC_ATTRIBUTE_MAP.POSITION:
{
const position = primitive.attributes.position.value;
polyData.getPoints().setData(position, primitive.attributes.position.component);
break;
}
case SEMANTIC_ATTRIBUTE_MAP.NORMAL:
{
const normals = primitive.attributes.normal.value;
pointData.setNormals(vtkDataArray.newInstance({
name: 'Normals',
values: normals,
numberOfComponents: primitive.attributes.normal.components
}));
break;
}
case SEMANTIC_ATTRIBUTE_MAP.COLOR_0:
{
const color = primitive.attributes.color.value;
pointData.setScalars(vtkDataArray.newInstance({
name: 'Scalars',
values: color,
numberOfComponents: primitive.attributes.color.components
}));
break;
}
case SEMANTIC_ATTRIBUTE_MAP.TEXCOORD_0:
{
const tcoords0 = primitive.attributes.texcoord0.value;
const da = vtkDataArray.newInstance({
name: 'TEXCOORD_0',
values: tcoords0,
numberOfComponents: primitive.attributes.texcoord0.components
});
pointData.addArray(da);
pointData.setActiveTCoords(da.getName());
break;
}
case SEMANTIC_ATTRIBUTE_MAP.TEXCOORD_1:
{
const tcoords = primitive.attributes.texcoord1.value;
const dac = vtkDataArray.newInstance({
name: 'TEXCOORD_1',
values: tcoords,
numberOfComponents: primitive.attributes.texcoord1.components
});
pointData.addArray(dac);
break;
}
case SEMANTIC_ATTRIBUTE_MAP.TANGENT:
{
const tangent = primitive.attributes.tangent.value;
const dat = vtkDataArray.newInstance({
name: 'Tangents',
values: tangent,
numberOfComponents: primitive.attributes.tangent.components
});
pointData.addArray(dat);
break;
}
default:
vtkWarningMacro(`Unhandled attribute: ${attributeName}`);
}
});
// Handle indices if available
if (primitive.indices != null) {
const indices = primitive.indices.value;
const nCells = indices.length - 2;
switch (primitive.mode) {
case MODES.GL_LINE_STRIP:
case MODES.GL_TRIANGLE_STRIP:
case MODES.GL_LINE_LOOP:
vtkWarningMacro('GL_LINE_LOOP not implemented');
break;
default:
cells.allocate(4 * indices.length / 3);
for (let cellId = 0; cellId < nCells; cellId += 3) {
const cell = indices.slice(cellId, cellId + 3);
cells.insertNextCell(cell);
}
}
}
switch (primitive.mode) {
case MODES.GL_TRIANGLES:
case MODES.GL_TRIANGLE_FAN:
polyData.setPolys(cells);
break;
case MODES.GL_LINES:
case MODES.GL_LINE_STRIP:
case MODES.GL_LINE_LOOP:
polyData.setLines(cells);
break;
case MODES.GL_POINTS:
polyData.setVerts(cells);
break;
case MODES.GL_TRIANGLE_STRIP:
polyData.setStrips(cells);
break;
default:
cells.delete();
vtkWarningMacro('Invalid primitive draw mode. Ignoring connectivity.');
}
return polyData;
}
/**
* Creates a VTK property from a GLTF material
* @param {object} model - The vtk model object
* @param {GLTFMaterial} material - The GLTF material
* @param {vtkActor} actor - The VTK actor
*/
async function createPropertyFromGLTFMaterial(model, material, actor) {
let metallicFactor = 1.0;
let roughnessFactor = 1.0;
const emissiveFactor = material.emissiveFactor;
const property = actor.getProperty();
const pbr = material.pbrMetallicRoughness;
if (pbr != null) {
if (!pbr?.metallicFactor || pbr?.metallicFactor <= 0 || pbr?.metallicFactor >= 1) {
vtkDebugMacro('Invalid material.pbrMetallicRoughness.metallicFactor value. Using default value instead.');
} else metallicFactor = pbr.metallicFactor;
if (!pbr?.roughnessFactor || pbr?.roughnessFactor <= 0 || pbr?.roughnessFactor >= 1) {
vtkDebugMacro('Invalid material.pbrMetallicRoughness.roughnessFactor value. Using default value instead.');
} else roughnessFactor = pbr.roughnessFactor;
const color = pbr.baseColorFactor;
if (color != null) {
property.setDiffuseColor(color[0], color[1], color[2]);
property.setOpacity(color[3]);
}
property.setMetallic(metallicFactor);
property.setRoughness(roughnessFactor);
property.setEmission(emissiveFactor);
if (pbr.baseColorTexture) {
pbr.baseColorTexture.extensions;
const tex = pbr.baseColorTexture.texture;
if (tex.extensions != null) {
const extensionsNames = Object.keys(tex.extensions);
extensionsNames.forEach(extensionName => {
// TODO: Handle KHR_texture_basisu extension
// const extension = tex.extensions[extensionName];
switch (extensionName) {
default:
vtkWarningMacro(`Unhandled extension: ${extensionName}`);
}
});
}
const sampler = tex.sampler;
const image = await loadImage(tex.source);
const diffuseTex = createVTKTextureFromGLTFTexture(image, sampler);
property.setDiffuseTexture(diffuseTex);
}
// Handle metallic-roughness texture (metallicRoughnessTexture)
if (pbr.metallicRoughnessTexture) {
pbr.metallicRoughnessTexture.extensions;
const tex = pbr.metallicRoughnessTexture.texture;
const sampler = tex.sampler;
const rmImage = await loadImage(tex.source);
const rmTex = createVTKTextureFromGLTFTexture(rmImage, sampler);
property.setRMTexture(rmTex);
}
// Handle ambient occlusion texture (occlusionTexture)
if (material.occlusionTexture) {
material.occlusionTexture.extensions;
const tex = material.occlusionTexture.texture;
const sampler = tex.sampler;
const aoImage = await loadImage(tex.source);
const aoTex = createVTKTextureFromGLTFTexture(aoImage, sampler);
property.setAmbientOcclusionTexture(aoTex);
}
// Handle emissive texture (emissiveTexture)
if (material.emissiveTexture) {
material.emissiveTexture.extensions;
const tex = material.emissiveTexture.texture;
const sampler = tex.sampler;
const emissiveImage = await loadImage(tex.source);
const emissiveTex = createVTKTextureFromGLTFTexture(emissiveImage, sampler);
property.setEmissionTexture(emissiveTex);
// Handle mutiple Uvs
if (material.emissiveTexture.texCoord != null) {
const pd = actor.getMapper().getInputData().getPointData();
pd.setActiveTCoords(`TEXCOORD_${material.emissiveTexture.texCoord}`);
}
}
// Handle normal texture (normalTexture)
if (material.normalTexture) {
material.normalTexture.extensions;
const tex = material.normalTexture.texture;
const sampler = tex.sampler;
const normalImage = await loadImage(tex.source);
const normalTex = createVTKTextureFromGLTFTexture(normalImage, sampler);
property.setNormalTexture(normalTex);
if (material.normalTexture.scale != null) {
property.setNormalStrength(material.normalTexture.scale);
}
}
}
// Material extensions
if (material.extensions != null) {
const extensionsNames = Object.keys(material.extensions);
extensionsNames.forEach(extensionName => {
const extension = material.extensions[extensionName];
switch (extensionName) {
case 'KHR_materials_unlit':
handleKHRMaterialsUnlit(extension, property);
break;
case 'KHR_materials_ior':
handleKHRMaterialsIor(extension, property);
break;
case 'KHR_materials_specular':
handleKHRMaterialsSpecular(extension, property);
break;
default:
vtkWarningMacro(`Unhandled extension: ${extensionName}`);
}
});
}
if (material.alphaMode !== ALPHA_MODE.OPAQUE) {
actor.setForceTranslucent(true);
}
property.setBackfaceCulling(!material.doubleSided);
}
/**
* Handles primitive extensions
* @param {string} nodeId The GLTF node id
* @param {*} extensions The extensions object
* @param {*} model The vtk model object
*/
function handlePrimitiveExtensions(nodeId, extensions, model) {
const extensionsNames = Object.keys(extensions);
extensionsNames.forEach(extensionName => {
const extension = extensions[extensionName];
switch (extensionName) {
case 'KHR_materials_variants':
model.variantMappings.set(nodeId, extension.mappings);
break;
case 'KHR_draco_mesh_compression':
break;
default:
vtkWarningMacro(`Unhandled extension: ${extensionName}`);
}
});
}
/**
* Creates a VTK actor from a GLTF mesh
* @param {GLTFMesh} mesh - The GLTF mesh
* @returns {vtkActor} The created VTK actor
*/
async function createActorFromGTLFNode(worldMatrix) {
const actor = vtkActor.newInstance();
const mapper = vtkMapper.newInstance();
mapper.setColorModeToDirectScalars();
mapper.setInterpolateScalarsBeforeMapping(true);
actor.setMapper(mapper);
actor.setUserMatrix(worldMatrix);
const polydata = vtkPolyData.newInstance();
mapper.setInputData(polydata);
return actor;
}
/**
* Creates a VTK actor from a GLTF mesh
* @param {GLTFMesh} mesh - The GLTF mesh
* @returns {vtkActor} The created VTK actor
*/
async function createActorFromGTLFPrimitive(model, primitive, worldMatrix) {
const actor = vtkActor.newInstance();
const mapper = vtkMapper.newInstance();
mapper.setColorModeToDirectScalars();
mapper.setInterpolateScalarsBeforeMapping(true);
actor.setMapper(mapper);
actor.setUserMatrix(worldMatrix);
const polydata = await createPolyDataFromGLTFMesh(primitive);
mapper.setInputData(polydata);
// Support for materials
if (primitive.material != null) {
await createPropertyFromGLTFMaterial(model, primitive.material, actor);
}
if (primitive.extensions != null) {
handlePrimitiveExtensions(`${primitive.name}`, primitive.extensions, model);
}
return actor;
}
/**
* Creates a GLTF animation object
* @param {GLTFAnimation} animation
* @returns
*/
function createGLTFAnimation(animation) {
vtkDebugMacro('Creating animation:', animation);
return {
name: animation.name,
channels: animation.channels,
samplers: animation.samplers,
getChannelByTargetNode(nodeIndex) {
return this.channels.filter(channel => channel.target.node === nodeIndex);
}
};
}
/**
* Gets the transformation matrix for a GLTF node
* @param {GLTFNode} node - The GLTF node
* @returns {mat4} The transformation matrix
*/
function getTransformationMatrix(node) {
// TRS
const translation = node.translation ?? vec3.create();
const rotation = node.rotation ?? quat.create();
const scale = node.scale ?? vec3.fromValues(1.0, 1.0, 1.0);
const matrix = node.matrix != null ? mat4.clone(node.matrix) : mat4.fromRotationTranslationScale(mat4.create(), rotation, translation, scale);
return matrix;
}
/**
* Processes a GLTF node
* @param {GLTFnode} node - The GLTF node
* @param {object} model The model object
* @param {vtkActor} parentActor The parent actor
* @param {mat4} parentMatrix The parent matrix
*/
async function processNode(node, model, parentActor = null, parentMatrix = mat4.create()) {
node.transform = getTransformationMatrix(node);
const worldMatrix = mat4.multiply(mat4.create(), parentMatrix, node.transform);
// Create actor for the current node
if (node.mesh != null) {
const nodeActor = await createActorFromGTLFNode(worldMatrix);
if (parentActor) {
nodeActor.setParentProp(parentActor);
}
model.actors.set(`${node.id}`, nodeActor);
await Promise.all(node.mesh.primitives.map(async (primitive, i) => {
const actor = await createActorFromGTLFPrimitive(model, primitive, worldMatrix);
actor.setParentProp(nodeActor);
model.actors.set(`${node.id}_${primitive.name}`, actor);
}));
}
// Handle KHRLightsPunctual extension
if (node.extensions?.KHR_lights_punctual) {
handleKHRLightsPunctual(node.extensions.KHR_lights_punctual, node.transform, model);
}
if (node.children && Array.isArray(node.children) && node.children.length > 0) {
await Promise.all(node.children.map(async child => {
const parent = model.actors.get(node.id);
await processNode(child, model, parent, worldMatrix);
}));
}
}
/**
* Creates VTK actors from a GLTF object
* @param {glTF} glTF - The GLTF object
* @param {number} sceneId - The scene index to create actors for
* @returns {vtkActor[]} The created VTK actors
*/
async function createVTKObjects(model) {
model.animations = model.glTFTree.animations?.map(createGLTFAnimation);
const extensionsNames = Object.keys(model.glTFTree?.extensions || []);
extensionsNames.forEach(extensionName => {
const extension = model.glTFTree.extensions[extensionName];
switch (extensionName) {
case 'KHR_materials_variants':
handleKHRMaterialsVariants(extension, model);
break;
case 'KHR_draco_mesh_compression':
break;
default:
vtkWarningMacro(`Unhandled extension: ${extensionName}`);
}
});
// Get the sceneId to process
const sceneId = model.sceneId ?? model.glTFTree.scene;
if (model.glTFTree.scenes?.length && model.glTFTree.scenes[sceneId]?.nodes) {
await Promise.all(model.glTFTree.scenes[sceneId].nodes.map(async node => {
if (node) {
await processNode(node, model);
} else {
vtkWarningMacro(`Node not found in glTF.nodes`);
}
}));
} else {
vtkWarningMacro('No valid scenes found in the glTF data');
}
}
/**
* Sets up the camera for a vtk renderer based on the bounds of the given actors.
*
* @param {GLTCamera} camera - The GLTF camera object
*/
function GLTFCameraToVTKCamera(glTFCamera) {
const camera = vtkCamera.newInstance();
if (glTFCamera.type === 'perspective') {
const {
yfov,
znear,
zfar
} = glTFCamera.perspective;
camera.setClippingRange(znear, zfar);
camera.setParallelProjection(false);
camera.setViewAngle(degreesFromRadians(yfov));
} else if (glTFCamera.type === 'orthographic') {
const {
ymag,
znear,
zfar
} = glTFCamera.orthographic;
camera.setClippingRange(znear, zfar);
camera.setParallelProjection(true);
camera.setParallelScale(ymag);
} else {
throw new Error('Unsupported camera type');
}
return camera;
}
/**
*
* @param {vtkCamera} camera
* @param {*} transformMatrix
*/
function applyTransformToCamera(camera, transformMatrix) {
if (!camera || !transformMatrix) {
return;
}
// At identity, camera position is origin, +y up, -z view direction
const position = [0, 0, 0];
const viewUp = [0, 1, 0];
const focus = [0, 0, -1];
const t = vtkTransform.newInstance();
t.setMatrix(transformMatrix);
// Transform position
t.transformPoint(position, position);
t.transformPoints(viewUp, viewUp);
t.transformPoints(focus, focus);
focus[0] += position[0];
focus[1] += position[1];
focus[2] += position[2];
// Apply the transformed values to the camera
camera.setPosition(position);
camera.setFocalPoint(focus);
camera.setViewUp(viewUp);
}
export { GLTFCameraToVTKCamera, applyTransformToCamera, createPropertyFromGLTFMaterial, createVTKObjects, parseGLTF };