Spaces:
Runtime error
Runtime error
| // Three.js GLB Generator | |
| // Generates GLB files using Three.js and GLTFExporter | |
| // Note: This requires three.js to be installed | |
| // If three.js is not available, the server will fallback to Blender | |
| export class ThreeJSGLBGenerator { | |
| constructor() { | |
| this.three = null; | |
| this.GLTFExporter = null; | |
| this.scene = null; | |
| this.objects = []; | |
| this.lights = []; | |
| } | |
| async loadThreeJS() { | |
| if (this.three && this.GLTFExporter) { | |
| return true; | |
| } | |
| try { | |
| const threeModule = await import('three'); | |
| const exporterModule = await import('three/examples/jsm/exporters/GLTFExporter.js'); | |
| this.three = threeModule; | |
| this.GLTFExporter = exporterModule.GLTFExporter; | |
| return true; | |
| } catch (error) { | |
| return false; | |
| } | |
| } | |
| async generate(sceneData, outputPath) { | |
| const fs = (await import('fs')).default; | |
| const loaded = await this.loadThreeJS(); | |
| if (!loaded) { | |
| throw new Error('Three.js is not available. Install with: npm install three'); | |
| } | |
| const THREE = this.three; | |
| const GLTFExporter = this.GLTFExporter; | |
| try { | |
| this.scene = new THREE.Scene(); | |
| this.objects = []; | |
| this.lights = []; | |
| this.setupEnvironment(sceneData, THREE); | |
| if (sceneData.ground) { | |
| this.createGround(sceneData.ground, THREE); | |
| } | |
| if (sceneData.objects) { | |
| sceneData.objects.forEach(obj => this.createObject(obj, THREE)); | |
| } | |
| if (sceneData.avatar && sceneData.avatar.present) { | |
| this.createAvatar(sceneData.avatar, THREE); | |
| } | |
| if (sceneData.lighting) { | |
| this.createLights(sceneData.lighting, THREE); | |
| } | |
| if (sceneData.camera) { | |
| this.createCamera(sceneData.camera, THREE); | |
| } | |
| await this.exportGLB(outputPath, THREE, GLTFExporter, fs); | |
| return { success: true, path: outputPath }; | |
| } catch (error) { | |
| return { success: false, error: error.message }; | |
| } | |
| } | |
| setupEnvironment(sceneData, THREE) { | |
| if (sceneData.lighting?.color) { | |
| this.scene.background = new THREE.Color( | |
| sceneData.lighting.color[0], | |
| sceneData.lighting.color[1], | |
| sceneData.lighting.color[2] | |
| ); | |
| } | |
| if (sceneData.effects?.fog) { | |
| this.scene.fog = new THREE.FogExp2( | |
| new THREE.Color(0.8, 0.8, 0.9), | |
| 0.05 | |
| ); | |
| } | |
| } | |
| createGround(groundData, THREE) { | |
| const size = groundData.size || 20; | |
| const geometry = new THREE.PlaneGeometry(size, size); | |
| const color = groundData.color || [0.5, 0.5, 0.5, 1]; | |
| const material = new THREE.MeshStandardMaterial({ | |
| color: new THREE.Color(color[0], color[1], color[2]), | |
| roughness: groundData.roughness || 0.8, | |
| metalness: groundData.metallic || 0.0 | |
| }); | |
| const mesh = new THREE.Mesh(geometry, material); | |
| mesh.rotation.x = -Math.PI / 2; | |
| mesh.name = "Ground"; | |
| this.scene.add(mesh); | |
| this.objects.push(mesh); | |
| } | |
| createObject(objData, THREE) { | |
| let geometry; | |
| switch (objData.type) { | |
| case 'cube': | |
| const size = objData.size || 1; | |
| geometry = new THREE.BoxGeometry(size, size, size); | |
| break; | |
| case 'sphere': | |
| geometry = new THREE.SphereGeometry(objData.radius || 0.5, 32, 32); | |
| break; | |
| case 'cylinder': | |
| geometry = new THREE.CylinderGeometry( | |
| objData.radius || 0.5, | |
| objData.radius || 0.5, | |
| objData.size || 1, | |
| 32 | |
| ); | |
| break; | |
| case 'cone': | |
| geometry = new THREE.ConeGeometry( | |
| objData.radius || 0.5, | |
| objData.size || 1, | |
| 32 | |
| ); | |
| break; | |
| case 'plane': | |
| geometry = new THREE.PlaneGeometry( | |
| objData.size || 10, | |
| objData.size || 10 | |
| ); | |
| break; | |
| default: | |
| geometry = new THREE.BoxGeometry(1, 1, 1); | |
| } | |
| const color = objData.color || [0.8, 0.8, 0.8, 1]; | |
| const material = new THREE.MeshStandardMaterial({ | |
| color: new THREE.Color(color[0], color[1], color[2]), | |
| roughness: objData.roughness || 0.5, | |
| metalness: objData.metallic || 0.0, | |
| transparent: color[3] < 1, | |
| opacity: color[3] || 1 | |
| }); | |
| if (objData.emission) { | |
| material.emissive = new THREE.Color( | |
| objData.emission[0], | |
| objData.emission[1], | |
| objData.emission[2] | |
| ); | |
| material.emissiveIntensity = objData.emissionStrength || 1.0; | |
| } | |
| const mesh = new THREE.Mesh(geometry, material); | |
| mesh.position.set( | |
| objData.location[0] || 0, | |
| objData.location[1] || 0, | |
| objData.location[2] || 0 | |
| ); | |
| mesh.rotation.set( | |
| objData.rotation[0] || 0, | |
| objData.rotation[1] || 0, | |
| objData.rotation[2] || 0 | |
| ); | |
| mesh.scale.set( | |
| objData.scale[0] || 1, | |
| objData.scale[1] || 1, | |
| objData.scale[2] || 1 | |
| ); | |
| mesh.name = objData.name || 'Object'; | |
| this.scene.add(mesh); | |
| this.objects.push(mesh); | |
| } | |
| createAvatar(avatarData, THREE) { | |
| const group = new THREE.Group(); | |
| group.name = avatarData.name || 'Avatar'; | |
| const torso = new THREE.Mesh( | |
| new THREE.CylinderGeometry(0.25, 0.25, 0.8, 32), | |
| new THREE.MeshStandardMaterial({ color: 0xffccaa }) | |
| ); | |
| torso.position.y = 1.0; | |
| group.add(torso); | |
| const head = new THREE.Mesh( | |
| new THREE.SphereGeometry(0.18, 32, 32), | |
| new THREE.MeshStandardMaterial({ color: 0xffccaa }) | |
| ); | |
| head.position.y = 1.9; | |
| group.add(head); | |
| const leftArm = new THREE.Mesh( | |
| new THREE.BoxGeometry(0.2, 0.4, 0.2), | |
| new THREE.MeshStandardMaterial({ color: 0xffccaa }) | |
| ); | |
| leftArm.position.set(-0.45, 1.3, 0); | |
| group.add(leftArm); | |
| const rightArm = new THREE.Mesh( | |
| new THREE.BoxGeometry(0.2, 0.4, 0.2), | |
| new THREE.MeshStandardMaterial({ color: 0xffccaa }) | |
| ); | |
| rightArm.position.set(0.45, 1.3, 0); | |
| group.add(rightArm); | |
| const leftLeg = new THREE.Mesh( | |
| new THREE.BoxGeometry(0.25, 0.75, 0.25), | |
| new THREE.MeshStandardMaterial({ color: 0x4444ff }) | |
| ); | |
| leftLeg.position.set(-0.18, 0.5, 0); | |
| group.add(leftLeg); | |
| const rightLeg = new THREE.Mesh( | |
| new THREE.BoxGeometry(0.25, 0.75, 0.25), | |
| new THREE.MeshStandardMaterial({ color: 0x4444ff }) | |
| ); | |
| rightLeg.position.set(0.18, 0.5, 0); | |
| group.add(rightLeg); | |
| group.position.set( | |
| avatarData.position[0] || 0, | |
| avatarData.position[1] || 0, | |
| avatarData.position[2] || 0 | |
| ); | |
| group.scale.set( | |
| avatarData.scale || 1, | |
| avatarData.scale || 1, | |
| avatarData.scale || 1 | |
| ); | |
| this.scene.add(group); | |
| this.objects.push(group); | |
| } | |
| createLights(lightingData, THREE) { | |
| const ambientLight = new THREE.AmbientLight( | |
| new THREE.Color(lightingData.color || [1, 1, 1]), | |
| lightingData.ambient_strength || 0.3 | |
| ); | |
| this.scene.add(ambientLight); | |
| this.lights.push(ambientLight); | |
| if (lightingData.lights && lightingData.lights.length > 0) { | |
| lightingData.lights.forEach(light => { | |
| let lightObj; | |
| switch (light.type) { | |
| case 'SUN': | |
| case 'DIRECTIONAL': | |
| lightObj = new THREE.DirectionalLight( | |
| new THREE.Color(light.color || lightingData.color || [1, 1, 1]), | |
| light.energy || 1 | |
| ); | |
| lightObj.position.set( | |
| light.location[0] || 10, | |
| light.location[1] || -10, | |
| light.location[2] || 10 | |
| ); | |
| break; | |
| case 'POINT': | |
| lightObj = new THREE.PointLight( | |
| new THREE.Color(light.color || lightingData.color || [1, 1, 1]), | |
| light.energy || 50, | |
| 100 | |
| ); | |
| lightObj.position.set( | |
| light.location[0] || 0, | |
| light.location[1] || 0, | |
| light.location[2] || 2 | |
| ); | |
| break; | |
| case 'SPOT': | |
| lightObj = new THREE.SpotLight( | |
| new THREE.Color(light.color || lightingData.color || [1, 1, 1]), | |
| light.energy || 50 | |
| ); | |
| lightObj.position.set( | |
| light.location[0] || 0, | |
| light.location[1] || 0, | |
| light.location[2] || 2 | |
| ); | |
| break; | |
| default: | |
| return; | |
| } | |
| if (lightObj) { | |
| this.scene.add(lightObj); | |
| this.lights.push(lightObj); | |
| } | |
| }); | |
| } | |
| } | |
| createCamera(cameraData, THREE) { | |
| const camera = new THREE.PerspectiveCamera( | |
| cameraData.fov || 50, | |
| 16 / 9, | |
| 0.1, | |
| 1000 | |
| ); | |
| camera.position.set( | |
| cameraData.position[0] || 4, | |
| cameraData.position[1] || -4, | |
| cameraData.position[2] || 2.2 | |
| ); | |
| camera.rotation.set( | |
| cameraData.rotation[0] || 1.05, | |
| cameraData.rotation[1] || 0, | |
| cameraData.rotation[2] || 0.78 | |
| ); | |
| this.scene.add(camera); | |
| } | |
| async exportGLB(outputPath, THREE, GLTFExporter, fs) { | |
| return new Promise((resolve, reject) => { | |
| // Polyfill FileReader for Node.js environment | |
| if (typeof global !== 'undefined' && !global.FileReader) { | |
| global.FileReader = class FileReader { | |
| constructor() { | |
| this.result = null; | |
| this.onload = null; | |
| this.onerror = null; | |
| } | |
| readAsArrayBuffer(blob) { | |
| // In Node.js, we'll handle this differently | |
| if (blob && blob.arrayBuffer) { | |
| blob.arrayBuffer().then(buffer => { | |
| this.result = buffer; | |
| if (this.onload) this.onload({ target: { result: buffer } }); | |
| }).catch(err => { | |
| if (this.onerror) this.onerror(err); | |
| }); | |
| } | |
| } | |
| }; | |
| } | |
| const exporter = new GLTFExporter(); | |
| const options = { | |
| binary: true, | |
| includeCustomExtensions: false, // Disable to avoid FileReader issues | |
| onlyVisible: false, | |
| truncateDrawRange: true, | |
| trs: false, | |
| animations: [] | |
| }; | |
| try { | |
| exporter.parse( | |
| this.scene, | |
| (result) => { | |
| try { | |
| // Handle both ArrayBuffer and Uint8Array | |
| let buffer; | |
| if (result instanceof ArrayBuffer) { | |
| buffer = Buffer.from(result); | |
| } else if (result instanceof Uint8Array) { | |
| buffer = Buffer.from(result); | |
| } else if (typeof result === 'string') { | |
| // If it's a string (JSON), convert to buffer | |
| buffer = Buffer.from(result, 'utf8'); | |
| } else if (result && result.buffer) { | |
| // Handle TypedArray | |
| buffer = Buffer.from(result.buffer); | |
| } else { | |
| // Try to convert directly | |
| buffer = Buffer.from(result); | |
| } | |
| if (!buffer || buffer.length === 0) { | |
| reject(new Error('GLB export resulted in empty buffer')); | |
| return; | |
| } | |
| fs.writeFileSync(outputPath, buffer); | |
| resolve(); | |
| } catch (error) { | |
| reject(new Error(`Failed to write GLB file: ${error.message}`)); | |
| } | |
| }, | |
| (error) => { | |
| // Check if error is about FileReader | |
| const errorMsg = error?.message || String(error); | |
| if (errorMsg.includes('FileReader')) { | |
| reject(new Error('GLTFExporter requires browser environment. Please install three.js properly or use Blender fallback.')); | |
| } else { | |
| reject(new Error(`GLTFExporter error: ${errorMsg}`)); | |
| } | |
| }, | |
| options | |
| ); | |
| } catch (error) { | |
| const errorMsg = error?.message || String(error); | |
| if (errorMsg.includes('FileReader')) { | |
| reject(new Error('GLTFExporter requires browser environment. Please install three.js properly.')); | |
| } else { | |
| reject(error); | |
| } | |
| } | |
| }); | |
| } | |
| } | |