Source: sceneExporter.js

import { BufferAttribute } from 'three/webgpu';
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter';
import { useStandardIndexType } from './utilities';

/*
 * GLTFExporter only writes POSITION and NORMAL morph targets. glTF allows
 * COLOR_n morph targets as well (GLTFLoader reads them), so add them to the
 * targets the exporter has already written. Like other glTF morph targets
 * they are stored relative to the base attribute.
 */
class GLTFMorphColourPlugin {
  constructor(writer) {
    this.writer = writer;
    this.name = 'ZINC_morph_colour';
  }

  writeMesh(mesh, meshDef) {
    const geometry = mesh.geometry;
    const morphColours =
      geometry && geometry.morphAttributes ? geometry.morphAttributes.color : undefined;
    const baseColour = geometry ? geometry.attributes.color : undefined;
    const primitive = meshDef.primitives ? meshDef.primitives[0] : undefined;
    if (!morphColours || !baseColour || !primitive || !primitive.targets) return;
    // All primitives of a mesh share the same targets array.
    const targets = primitive.targets;
    const itemSize = baseColour.itemSize;
    const count = baseColour.count;
    for (let i = 0; i < targets.length && i < morphColours.length; i++) {
      const attribute = morphColours[i];
      if (attribute.itemSize !== itemSize || attribute.count < count) {
        console.warn('GLTFExporter: Colour morph target does not match the base colour, skipped.');
        return;
      }
      const array = new Float32Array(count * itemSize);
      for (let j = 0; j < count; j++) {
        for (let a = 0; a < itemSize; a++) {
          const value = attribute.getComponent(j, a);
          array[j * itemSize + a] = geometry.morphTargetsRelative
            ? value
            : value - baseColour.getComponent(j, a);
        }
      }
      targets[i].COLOR_0 = this.writer.processAccessor(
        new BufferAttribute(array, itemSize),
        geometry,
      );
    }
  }
}

/**
 * Provides an object which uses for exporting the scene
 *
 * @class
 * @author Alan Wu
 * @return {SceneExporter}
 */
const SceneExporter = function (sceneIn) {
  const scene = sceneIn;

  // Matches the exporter's onlyVisible pruning - animations targeting a
  // hidden node would otherwise produce channels with no target node.
  const isVisible = (object) => {
    for (let current = object; current; current = current.parent) {
      if (current.visible === false) return false;
    }
    return true;
  };

  this.exportGLTF = async (binary) => {
    const rootRegion = scene.getRootRegion();
    const zincObjects = rootRegion.getAllObjects(true);
    const animations = [];
    const glyphsetsToRestore = [];
    for (const zincObject of zincObjects) {
      if (zincObject.animationClip && zincObject.animationClip[0]) {
        const mesh = zincObject.getMorph();
        if (mesh && isVisible(mesh)) {
          // The exporter resolves tracks against the scene root, but morph
          // clips target '' (the mesh itself), so retarget them by uuid.
          const clip = zincObject.animationClip[0].clone();
          clip.tracks.forEach((track) => {
            track.name = mesh.uuid + track.name;
          });
          animations.push(clip);
        }
      }
      // Glyphsets with GPU-compute-driven colour intentionally keep
      // instanceColor null during normal rendering (see glyphset.js's
      // prepareColorForExport() for why) - populate it just for this
      // export, then undo it again afterwards.
      if (zincObject.isGlyphset && zincObject.prepareColorForExport) {
        await zincObject.prepareColorForExport();
        glyphsetsToRestore.push(zincObject);
      }
    }
    //GLTFExporter only accepts plain typed arrays
    const restoreIndexType = useStandardIndexType(scene.getThreeJSScene());
    const exporter = new GLTFExporter();
    exporter.register((writer) => new GLTFMorphColourPlugin(writer));
    const options = { binary, animations, onlyVisible: true };
    try {
      return await new Promise((resolve, reject) => {
        exporter.parse(
          scene.getThreeJSScene(),
          function (gltf) {
            resolve(gltf);
          },
          function (error) {
            console.error('Unable to export GLTF.', error);
            reject(error);
          },
          options,
        );
      });
    } finally {
      restoreIndexType();
      glyphsetsToRestore.forEach((zincObject) => zincObject.clearColorExportState());
    }
  };
};

export { SceneExporter };