Skip to main content
Glama

3D Asset Processing MCP

by GeoLibra
analyzer.ts14.3 kB
import { NodeIO, Document } from '@gltf-transform/core'; import { ModelAnalysis, ProcessResult } from '../types'; import { globalCache } from '../utils/cache'; import logger from '../utils/logger'; import { GLB_EXT } from '../utils/gltf-constants'; export class ModelAnalyzer { private io: NodeIO; private initialized: Promise<void>; constructor() { // Initialize IO this.io = new NodeIO(); this.initialized = this.initializeAsync(); } private async initializeAsync(): Promise<void> { const isTestEnv = process.env.JEST_WORKER_ID !== undefined || process.env.NODE_ENV === 'test'; if (!isTestEnv) { try { // Dynamically import and register extensions const { ALL_EXTENSIONS } = await import('@gltf-transform/extensions'); this.io.registerExtensions(ALL_EXTENSIONS); logger.debug('Registered ALL_EXTENSIONS successfully'); // Register decoders const dependencies: Record<string, any> = {}; // Try to register Draco decoder try { // eslint-disable-next-line @typescript-eslint/no-var-requires const draco3d = require('draco3dgltf'); if (draco3d && draco3d.createDecoderModule) { dependencies['draco3d.decoder'] = await draco3d.createDecoderModule(); logger.debug('Registered Draco decoder'); } } catch (e) { logger.warn('Draco decoder not available:', e instanceof Error ? e.message : String(e)); } // Try to register Meshopt decoder try { // eslint-disable-next-line @typescript-eslint/no-var-requires const meshopt = require('meshoptimizer'); if (meshopt && meshopt.MeshoptDecoder) { dependencies['meshopt.decoder'] = meshopt.MeshoptDecoder; logger.debug('Registered Meshopt decoder'); } } catch (e) { try { // Fallback to meshopt_decoder package // eslint-disable-next-line @typescript-eslint/no-var-requires const meshoptDecoder = require('meshopt_decoder'); dependencies['meshopt.decoder'] = meshoptDecoder.default || meshoptDecoder.MeshoptDecoder || meshoptDecoder; logger.debug('Registered Meshopt decoder (fallback)'); } catch (e2) { logger.warn('Meshopt decoder not available:', e instanceof Error ? e.message : String(e)); } } // Register all available dependencies if (Object.keys(dependencies).length > 0) { this.io.registerDependencies(dependencies); logger.debug(`Registered ${Object.keys(dependencies).length} decoders`); } } catch (e) { const msg = e instanceof Error ? e.message : String(e); logger.warn(`Failed to initialize extensions and decoders: ${msg}`); } } else { logger.debug('Skipping extensions registration in test environment'); } } /** * Analyze 3D model */ async analyze(filePath: string): Promise<ProcessResult> { const startTime = Date.now(); try { // Wait for initialization to complete await this.initialized; // Check cache const cacheKey = globalCache.generateKey('analysis', filePath); const cached = globalCache.get<ModelAnalysis>(cacheKey); if (cached) { logger.info(`Analysis cache hit for: ${filePath}`); return { success: true, data: cached, metrics: { processingTime: Date.now() - startTime, memoryUsage: process.memoryUsage().heapUsed } }; } logger.info(`Starting analysis for: ${filePath}`); // Read document const document = await this.io.read(filePath); // Perform analysis const analysis = await this.performAnalysis(document, filePath); // Cache result globalCache.set(cacheKey, analysis, 3600); // 1小时缓存 const processingTime = Date.now() - startTime; logger.info(`Analysis completed in ${processingTime}ms for: ${filePath}`); return { success: true, data: analysis, metrics: { processingTime, memoryUsage: process.memoryUsage().heapUsed } }; } catch (error) { logger.error(`Analysis failed for ${filePath}:`, error); return { success: false, error: error instanceof Error ? error.message : String(error), metrics: { processingTime: Date.now() - startTime, memoryUsage: process.memoryUsage().heapUsed } }; } } /** * Perform detailed analysis */ private async performAnalysis(document: Document, filePath: string): Promise<ModelAnalysis> { const root = document.getRoot(); // Basic metadata const metadata = this.analyzeMetadata(document, filePath); // Geometry analysis const geometry = this.analyzeGeometry(root); // Materials analysis const materials = this.analyzeMaterials(root); // Texture analysis const textures = this.analyzeTextures(root); // Animation analysis const animations = this.analyzeAnimations(root); // Extensions analysis const extensions = this.analyzeExtensions(root); // Performance analysis const performance = this.analyzePerformance(root, geometry, textures); return { geometry: { vertexCount: geometry.vertexCount, triangleCount: geometry.triangleCount, meshCount: geometry.meshCount, primitiveCount: geometry.primitiveCount }, materials: { count: materials.materialCount, types: ['PBR', 'Unlit'], textureCount: textures.textureCount, totalTextureSize: textures.totalTextureSize }, animations: { count: animations.animationCount, totalKeyframes: animations.samplers, duration: animations.totalDuration }, scene: { nodeCount: root.listNodes().length, maxDepth: this.calculateSceneDepth(root) }, extensions: { used: extensions.used, required: extensions.required, count: extensions.used.length }, fileInfo: { size: metadata.fileSize, format: metadata.format, version: metadata.version } }; } /** * Analyze metadata */ private analyzeMetadata(document: Document, filePath: string) { const root = document.getRoot(); const asset = root.getAsset(); // Get file size const fs = require('fs'); const stats = fs.statSync(filePath); return { fileSize: stats.size, format: filePath.endsWith(GLB_EXT) ? 'GLB' as const : 'glTF' as const, version: asset.version || '2.0', generator: asset.generator || 'Unknown' }; } /** * Analyze geometry data */ private analyzeGeometry(root: any) { const meshes = root.listMeshes(); let primitiveCount = 0; let vertexCount = 0; let triangleCount = 0; let hasNormals = false; let hasTangents = false; let hasTexCoords = false; let hasColors = false; for (const mesh of meshes) { const primitives = mesh.listPrimitives(); primitiveCount += primitives.length; for (const primitive of primitives) { const position = primitive.getAttribute('POSITION'); if (position) { vertexCount += position.getCount(); } const indices = primitive.getIndices(); if (indices) { triangleCount += indices.getCount() / 3; } else if (position) { triangleCount += position.getCount() / 3; } // Check attributes if (primitive.getAttribute('NORMAL')) hasNormals = true; if (primitive.getAttribute('TANGENT')) hasTangents = true; if (primitive.getAttribute('TEXCOORD_0')) hasTexCoords = true; if (primitive.getAttribute('COLOR_0')) hasColors = true; } } return { meshCount: meshes.length, primitiveCount, vertexCount, triangleCount: Math.floor(triangleCount), hasNormals, hasTangents, hasTexCoords, hasColors }; } /** * Analyze materials */ private analyzeMaterials(root: any) { const materials = root.listMaterials(); let pbrMaterials = 0; let unlitMaterials = 0; const extensions = new Set<string>(); for (const material of materials) { if (material.getBaseColorTexture() || material.getMetallicRoughnessTexture()) { pbrMaterials++; } // Check unlit extension if (material.getExtension('KHR_materials_unlit')) { unlitMaterials++; extensions.add('KHR_materials_unlit'); } // Check other material extensions const materialExtensions = [ 'KHR_materials_clearcoat', 'KHR_materials_transmission', 'KHR_materials_volume', 'KHR_materials_ior', 'KHR_materials_specular', 'KHR_materials_sheen' ]; for (const ext of materialExtensions) { if (material.getExtension(ext)) { extensions.add(ext); } } } return { materialCount: materials.length, pbrMaterials, unlitMaterials, extensions: Array.from(extensions) }; } /** * Analyze textures */ private analyzeTextures(root: any) { const textures = root.listTextures(); let totalTextureSize = 0; const formats: Record<string, number> = {}; const colorSpaces: Record<string, number> = {}; let maxResolution: [number, number] = [0, 0]; for (const texture of textures) { const image = texture.getImage(); if (image) { const size = image.byteLength; totalTextureSize += size; // Try to get image information (simplified handling) const mimeType = texture.getMimeType(); if (mimeType) { formats[mimeType] = (formats[mimeType] || 0) + 1; } // Color space analysis (simplified) const colorSpace = 'sRGB'; // Default, actually requires more complex detection colorSpaces[colorSpace] = (colorSpaces[colorSpace] || 0) + 1; } } return { textureCount: textures.length, totalTextureSize, formats, maxResolution, colorSpaces }; } /** * Analyze animations */ private analyzeAnimations(root: any) { const animations = root.listAnimations(); let totalDuration = 0; let channels = 0; let samplers = 0; for (const animation of animations) { const animChannels = animation.listChannels(); const animSamplers = animation.listSamplers(); channels += animChannels.length; samplers += animSamplers.length; // Calculate animation duration for (const sampler of animSamplers) { const input = sampler.getInput(); if (input) { const times = input.getArray(); if (times && times.length > 0) { const timeArray = Array.from(times) as number[]; const duration = Math.max(...timeArray); totalDuration = Math.max(totalDuration, duration); } } } } return { animationCount: animations.length, totalDuration, channels, samplers }; } /** * Analyze extensions */ private analyzeExtensions(root: any) { const usedExtensions = root.listExtensionsUsed(); const requiredExtensions = root.listExtensionsRequired(); // Extract extension names from extension objects const used = usedExtensions ? usedExtensions.map((ext: any) => ext.extensionName || ext) : []; const required = requiredExtensions ? requiredExtensions.map((ext: any) => ext.extensionName || ext) : []; // Log extensions for debugging if (used && used.length > 0) { logger.info(`Extensions used: ${used.join(', ')}`); } if (required && required.length > 0) { logger.info(`Extensions required: ${required.join(', ')}`); } return { used, required }; } /** * Calculate the maximum depth of the scene graph */ private calculateSceneDepth(root: any): number { const scenes = root.listScenes(); let maxDepth = 0; for (const scene of scenes) { // Get root nodes of the scene (nodes without parents) const rootNodes = root.listNodes().filter((node: any) => !node.getParent()); for (const node of rootNodes) { const depth = this.getNodeDepth(node, 1); maxDepth = Math.max(maxDepth, depth); } } return maxDepth || 1; } /** * Get the depth of a node in the scene graph */ private getNodeDepth(node: any, currentDepth: number): number { let maxChildDepth = currentDepth; const children = node.listChildren(); for (const child of children) { const childDepth = this.getNodeDepth(child, currentDepth + 1); maxChildDepth = Math.max(maxChildDepth, childDepth); } return maxChildDepth; } /** * Performance analysis */ private analyzePerformance(root: any, geometry: any, textures: any) { const bottlenecks: string[] = []; const recommendations: string[] = []; // Estimate Draw Calls const estimatedDrawCalls = geometry.primitiveCount; // Estimate memory usage const estimatedMemoryUsage = textures.totalTextureSize + (geometry.vertexCount * 32); // Simplified estimation // Performance bottleneck detection if (geometry.triangleCount > 100000) { bottlenecks.push('High triangle count'); recommendations.push('Consider using geometry simplification'); } if (textures.totalTextureSize > 50 * 1024 * 1024) { // 50MB bottlenecks.push('Large texture memory usage'); recommendations.push('Consider texture compression or resizing'); } if (estimatedDrawCalls > 100) { bottlenecks.push('High draw call count'); recommendations.push('Consider mesh merging or instancing'); } if (!geometry.hasNormals) { recommendations.push('Consider adding normals for better lighting'); } return { estimatedDrawCalls, estimatedMemoryUsage, bottlenecks, recommendations }; } } // Global analyzer instance export const globalAnalyzer = new ModelAnalyzer();

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/GeoLibra/3d-asset-processing-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server