@vctrl/core
Server-side 3D model processing for Node.js. This package provides the shared loading, optimization, and export pipeline used by other Vectreal packages.
Installation
npm install @vctrl/core
# or
pnpm add @vctrl/core
@vctrl/coretargets Node.js only. It uses Sharp for server-side texture compression, which requires a native build.
Module overview
| Module | Import path | Description |
|---|---|---|
ModelLoader | @vctrl/core/model-loader | Load model files into glTF-Transform Document or Three.js scenes |
ModelOptimizer | @vctrl/core/model-optimizer | Run optimization passes and export optimized output |
ModelExporter | @vctrl/core/model-exporter | Export Document or Three.js objects to GLB/GLTF |
SceneAsset | @vctrl/core | Scene asset serialization helpers and shared server payload types |
ModelLoader
import { ModelLoader } from '@vctrl/core/model-loader'
import { readFile } from 'node:fs/promises'
const loader = new ModelLoader()
// From a file path
const result = await loader.loadFromFile('model.glb')
// From a Node.js Buffer
const buffer = await readFile('model.glb')
const resultFromBuffer = await loader.loadFromBuffer(
new Uint8Array(buffer),
'model.glb'
)
// Convert directly to Three.js scene
const sceneResult = await loader.loadToThreeJS('model.glb')Primary methods
| Method | Description |
|---|---|
loadFromFile(input) | Load from file path (Node) or browser File |
loadFromBuffer(buffer, fileName) | Load from Uint8Array data |
loadGLTFWithAssets(...) / loadGLTFWithFileAssets(...) | Load GLTF with external resources |
documentToThreeJS(document, modelResult) | Convert glTF-Transform Document to Three.js scene |
loadToThreeJS(input) | Load and convert to Three.js scene |
loadGLTFWithAssetsToThreeJS(...) | GLTF + assets directly to Three.js |
isSupportedFormat(fileName) | Validate extension support |
getSupportedExtensions() | Return supported extensions |
documentToThreeJS requires both the Document and the original ModelLoadResult metadata object:
const loaded = await loader.loadFromFile('model.glb')
const threeResult = await loader.documentToThreeJS(loaded.data, loaded)ModelOptimizer
import { ModelOptimizer } from '@vctrl/core/model-optimizer'
const optimizer = new ModelOptimizer()
// Load a model into the optimizer
await optimizer.loadFromBuffer(modelBuffer)
// Run optimizations
await optimizer.simplify({ ratio: 0.5 })
await optimizer.deduplicate()
await optimizer.quantize({ quantizePosition: 14 })
await optimizer.compressTextures({ quality: 80 })
// Export the result
const optimizedBuffer = await optimizer.export()Methods
| Method | Options | Description |
|---|---|---|
loadFromThreeJS(model) | — | Load a Three.js scene into optimizer |
loadFromBuffer(buf) | — | Load GLB binary data |
loadFromFile(path) | — | Load from file path |
loadFromJSON(json) | — | Load serialized glTF JSON/resources |
simplify(opts) | { ratio: number } | Mesh simplification (0.0–1.0) |
deduplicate(opts) | DedupOptions | Remove duplicate geometry/material data |
quantize(opts) | QuantizeOptions | Reduce precision to reduce size |
optimizeNormals(opts) | NormalsOptions | Recompute/normalize normal data |
compressTextures(opts) | TextureCompressOptions | Server-side texture compression |
optimizeAll(opts) | { simplify?, dedup?, quantize?, normals?, textures? } | Run batch optimization passes |
getReport() | — | Return before/after optimization metrics |
export() / exportJSON() | — | Export optimized GLB or JSON glTF document |
hasModel() / reset() | — | Model state utilities |
Optimization options reference
simplify(options?: SimplifyOptions)
| Option | Type | Default | Notes |
|---|---|---|---|
ratio | number | 0.5 | Target simplification ratio. Lower values are more aggressive. |
error | number | 0.001 | Allowed geometric error threshold for simplification. |
deduplicate(options?: DedupOptions)
| Option | Type | Notes |
|---|---|---|
textures | boolean | Forwarded to glTF-Transform dedup() |
materials | boolean | Forwarded to glTF-Transform dedup() |
meshes | boolean | Forwarded to glTF-Transform dedup() |
accessors | boolean | Forwarded to glTF-Transform dedup() |
quantize(options?: QuantizeOptions)
| Option | Type | Notes |
|---|---|---|
quantizePosition | number | Forwarded to glTF-Transform quantize() |
quantizeNormal | number | Forwarded to glTF-Transform quantize() |
quantizeColor | number | Forwarded to glTF-Transform quantize() |
quantizeTexcoord | number | Forwarded to glTF-Transform quantize() |
optimizeNormals(options?: NormalsOptions)
| Option | Type | Notes |
|---|---|---|
overwrite | boolean | Recompute normals even when normals already exist |
compressTextures(options?: TextureCompressOptions)
| Option | Type | Current behavior |
|---|---|---|
resize | [number, number] | Passed to compressTexture() |
targetFormat | `'webp' | 'jpeg' |
quality | number | Passed to compressTexture() |
requestTimeoutMs | number | Present in type but not consumed by current ModelOptimizer implementation |
maxTextureUploadBytes | number | Present in type but not consumed by current ModelOptimizer implementation |
maxRetries | number | Present in type but not consumed by current ModelOptimizer implementation |
maxConcurrentRequests | number | Present in type but not consumed by current ModelOptimizer implementation |
serverOptions | ServerOptions | Accepted in type but stripped before local Sharp compression |
When Sharp is unavailable, compressTextures falls back to basic texture optimization (dedup + prune) instead of throwing.
optimizeAll(options?)
await optimizer.optimizeAll({
simplify: { ratio: 0.6 },
dedup: {},
quantize: { quantizePosition: 14 },
normals: { overwrite: false },
textures: { targetFormat: 'webp', quality: 80 },
})Execution order is fixed:
simplify(unlessfalse)deduplicate(unlessfalse)quantize(unlessfalse)optimizeNormals(unlessfalse)compressTextures(only whentexturesis provided)
Calling optimizeAll() with no arguments runs simplify, dedup, quantize, and normals. Texture compression is opt-in.
getReport() return structure
getReport() includes:
originalSize,optimizedSizecompressionRatio(originalSize / optimizedSize)appliedOptimizationsstatsbefore/after metrics for vertices, triangles, materials, textures (size),texturesCount,textureResolutions, meshes, and nodes
const report = await optimizer.getReport()
console.log(report.stats.textureResolutions.before)
console.log(report.stats.textureResolutions.after)ModelExporter
import { ModelExporter } from '@vctrl/core/model-exporter'
const exporter = new ModelExporter()
// Export Three.js scene as binary GLB
const glb = await exporter.exportThreeJSGLB(scene, {})
// Export Three.js scene as glTF + assets map
const gltf = await exporter.exportThreeJSGLTF(scene)
// Package glTF + assets into zip
const zip = await exporter.createZIPArchive(gltf, 'model')Primary methods
| Method | Description |
|---|---|
exportDocumentGLB(document) | Export glTF-Transform Document to GLB |
exportDocumentGLTF(document) | Export Document to GLTF JSON + assets |
exportThreeJSGLB(object, options) | Export Three.js object to GLB |
exportThreeJSGLTF(object) | Export Three.js object to GLTF JSON + assets |
createZIPArchive(result, baseName?) | Bundle GLTF + assets into zip |
saveToFile(result, filePath) | Persist export result on Node filesystem |
exportThreeJSGLB(object, options) accepts modifiedTextureResources in its options type, but that field is currently ignored for direct Three.js GLB export.
Use in API routes
@vctrl/core is used by the Vectreal Platform's server-side optimization endpoint. Here's a minimal example of using it in a Node.js API route:
import { ModelOptimizer } from '@vctrl/core/model-optimizer'
export async function POST(request: Request) {
const formData = await request.formData()
const file = formData.get('file') as File
const buffer = Buffer.from(await file.arrayBuffer())
const optimizer = new ModelOptimizer()
await optimizer.loadFromBuffer(new Uint8Array(buffer))
await optimizer.simplify({ ratio: 0.7 })
await optimizer.compressTextures({ quality: 80 })
const optimized = await optimizer.export()
return new Response(optimized, {
headers: { 'Content-Type': 'model/gltf-binary' },
})
}Requirements
| Requirement | Version |
|---|---|
| Node.js | 18 or later |
sharp | ^0.34 |
Source
Full source and README in packages/core/.
Related docs
- Deployment — where
@vctrl/corepowers server-side build/runtime flows - Publishing & Embedding — preview and serving model output
- @vctrl/hooks — browser/runtime companion API