import * as path from 'path' import * as fs from 'fs' import { NodeIO, Document, ImageUtils } from '@gltf-transform/core' import { createEncoderModule, createDecoderModule } from 'draco3dgltf' import { wrapper, TypeOnEveryFile, TypeToolProp, TypeToolOptions } from '@asset-toolkit/core' /** * ### A tool to transform gltf/glb files. * This tool supports reading, editing, and writing 3D models in glTF 2.0 format * * https://gltf-transform.donmccurdy.com/ * * ### Example * *```js const { pipeline } = require("@asset-toolkit/core"); const gltfTransform = require("@asset-toolkit/gltf-transform"); *``` * * ```js const pipelineGTLF = pipeline(gltfTransform); pipelineGTLF({ pathIn: "./gltf/", pathOut: "../public/gltf", }); * ``` * * ### Useful Example * In combination with image-supercompression tool to transform all images attached (supercompressed ktx2 image). * Use cloneFiles tool utility to copy all element from origin folder to output folder. * Passing the options to gltf-transform to activate the basis extension. * *```js const { pipeline, cloneFiles } = require("@asset-toolkit/core"); const gltfTransform = require("@asset-toolkit/gltf-transform"); *``` * * ```js const pipelineGTLF = pipeline( imageSuperCompression, (props) => cloneFiles({ pathIn: "./gltf/", ...props }), gltfTransform ); pipelineGTLF( { pathIn: "./gltf/", pathOut: "../public/gltf", }, [, , { basis: true }] ); * * @module */ function toBuffer(ab: ArrayBuffer) { const buf = Buffer.alloc(ab.byteLength) const view = new Uint8Array(ab) for (let i = 0; i < buf.length; ++i) { buf[i] = view[i] } return buf } export interface TypeProps extends TypeToolOptions { draco?: number | boolean applyBasis?: boolean save?: boolean saveJSON?: boolean saveBinary?: boolean extractTextures?: boolean callback?: (a: Document, b: string) => void } /** * A tool to minify the json file. * @param props - { pathIn, pathOut } * @param options - { } * @returns { Promise(returns) } */ const gltfTransform: (_props: TypeToolProp, options?: TypeProps) => Promise = wrapper( ({ pathIn, pathOut, options, file, next, wrapperProps, log }: TypeOnEveryFile) => { const process = async () => { const io = new NodeIO() const { DracoMeshCompression } = require('@gltf-transform/extensions') if (!wrapperProps.draco3dDecoder) wrapperProps.draco3dDecoder = await createDecoderModule() if (!wrapperProps.draco3dEncoder) wrapperProps.draco3dEncoder = await createEncoderModule() io.registerExtensions([DracoMeshCompression]).registerDependencies({ 'draco3d.decoder': wrapperProps.draco3dDecoder, 'draco3d.encoder': wrapperProps.draco3dEncoder, }) const subProcess = async () => { const opts = { save: true, ...options, } const { extractTextures, applyBasis, draco, callback, save, saveJSON, saveBinary } = opts as TypeProps const doc = io.read(path.resolve(pathIn || pathOut, file)) if (applyBasis) { const { TextureBasisu } = require('@gltf-transform/extensions') const basisuExtension = doc.createExtension(TextureBasisu).setRequired(true) doc .getRoot() .listTextures() .map((texture, key) => { const ext = texture.getMimeType() === 'image/png' ? 'png' : 'jpg' let name = texture.getName() if (!name) texture.setName(`image_${key}`) const file = `${name}.${ext}.ktx2` let pathCurrent = pathOut if (pathIn && fs.existsSync(path.resolve(pathIn, file))) { pathCurrent = pathIn } if (fs.existsSync(path.resolve(pathCurrent, file))) { const ktx2Image = fs.readFileSync(path.resolve(pathCurrent, file)) texture.setMimeType('image/ktx2').setImage(ktx2Image) fs.rmSync(path.resolve(pathCurrent, file)) } return texture }) } else { doc .getRoot() .listTextures() .map((texture, key) => { const ext = texture.getMimeType() === 'image/png' ? 'png' : 'jpg' const name = texture.getName() const file = `${name}.${ext}` if (fs.existsSync(path.resolve(pathOut, file))) { texture.setMimeType(texture.getMimeType()).setImage(fs.readFileSync(path.resolve(pathOut, file))) fs.rmSync(path.resolve(pathOut, file)) } return texture }) } if (extractTextures) { doc .getRoot() .listTextures() .map((texture, key) => { const ext = texture.getMimeType() === 'image/png' ? 'png' : 'jpg' const image = texture.getImage() let name = texture.getName() if (!name) texture.setName(`image_${key}`) log(`${texture.getName()}.${ext}`) if (image) { fs.writeFileSync(path.resolve(pathOut, `${texture.getName()}.${ext}`), toBuffer(image)) } return texture }) } const ext = path.extname(file) if (draco) { const { DracoMeshCompression } = require('@gltf-transform/extensions') doc .createExtension(DracoMeshCompression) .setRequired(true) //@ts-ignore .setEncoderOptions({ method: DracoMeshCompression.EncoderMethod.EDGEBREAKER, encodeSpeed: typeof draco === 'boolean' ? 5 : draco, decodeSpeed: 5, }) } if (callback) callback(doc, file) if (save) { io.write(path.resolve(pathOut, file.replace(ext, '.glb')), doc) } if (saveJSON) { fs.writeFileSync(path.resolve(pathOut, file.replace(ext, '.json')), JSON.stringify(io.writeJSON(doc))) } if (saveBinary) { fs.writeFileSync(path.resolve(pathOut, file.replace(ext, '.bin')), Buffer.from(io.writeBinary(doc))) } next() } subProcess() } process() }, undefined, { exts: ['gltf', 'glb'], name: 'GLTF Transform', draco3dDecoder: null, draco3dEncoder: null }, ) export default gltfTransform