mirror of
https://github.com/manyfold3d/manyfold.git
synced 2026-10-22 03:32:18 -05:00
285 lines
8.9 KiB
TypeScript
285 lines
8.9 KiB
TypeScript
import * as Comlink from 'comlink'
|
|
import 'src/comlink_event_handler'
|
|
|
|
import * as THREE from 'three'
|
|
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js'
|
|
import { STLLoader } from 'three/addons/loaders/STLLoader.js'
|
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
|
|
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'
|
|
import { ThreeMFLoader } from 'threejs-webworker-3mf-loader'
|
|
import { PLYLoader } from 'three/addons/loaders/PLYLoader.js'
|
|
import { TDSLoader } from 'three/addons/loaders/TDSLoader.js'
|
|
import { FBXLoader } from 'three/addons/loaders/FBXLoader.js'
|
|
import { GCodeLoader } from 'three/addons/loaders/GCodeLoader.js'
|
|
import { LDrawLoader } from 'three/addons/loaders/LDrawLoader.js'
|
|
|
|
import { OrbitControls } from 'src/orbit_controls.js'
|
|
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'
|
|
import { SSAOPass } from 'three/addons/postprocessing/SSAOPass.js'
|
|
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'
|
|
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'
|
|
import { CanvasProxy } from 'src/canvas_proxy'
|
|
import { LDrawConditionalLineMaterial } from 'three/addons/materials/LDrawConditionalLineMaterial.js';
|
|
|
|
const loaders = {
|
|
'3mf': ThreeMFLoader,
|
|
drc: DRACOLoader,
|
|
gltf: GLTFLoader,
|
|
glb: GLTFLoader,
|
|
obj: OBJLoader,
|
|
stl: STLLoader,
|
|
ply: PLYLoader,
|
|
'3ds': TDSLoader,
|
|
fbx: FBXLoader,
|
|
gcode: GCodeLoader,
|
|
ldr: LDrawLoader,
|
|
mpd: LDrawLoader
|
|
}
|
|
|
|
export class OffscreenRenderer {
|
|
canvas: CanvasProxy
|
|
renderer: THREE.WebGLRenderer
|
|
settings: DOMStringMap
|
|
scene: THREE.Scene
|
|
composer: THREE.EffectComposer
|
|
camera: THREE.PerspectiveCamera
|
|
controls: OrbitControls
|
|
gridHelper: THREE.GridHelper
|
|
ready: boolean = false
|
|
|
|
cbLoadComplete: any = null
|
|
cbLoadProgress: any = null
|
|
cbLoadError: any = null
|
|
|
|
constructor (
|
|
canvas: HTMLCanvasElement,
|
|
settings: DOMStringMap
|
|
) {
|
|
this.canvas = new CanvasProxy(canvas)
|
|
this.renderer = new THREE.WebGLRenderer({ canvas: this.canvas.realCanvas })
|
|
this.settings = settings
|
|
this.setup()
|
|
}
|
|
|
|
handleEvent (event): void {
|
|
this.canvas.handleEvent(event)
|
|
}
|
|
|
|
setup (): void {
|
|
this.scene = new THREE.Scene()
|
|
this.scene.background = new THREE.Color(this.settings.backgroundColour ?? '#000000')
|
|
this.camera = new THREE.PerspectiveCamera(
|
|
45,
|
|
this.canvas.clientWidth / this.canvas.clientHeight,
|
|
0.1,
|
|
100000
|
|
)
|
|
this.controls = new OrbitControls(this.camera, this.canvas as any)
|
|
this.controls.enableDamping = true
|
|
this.controls.enablePan = this.controls.enableZoom = (this.settings.enablePanZoom === 'true')
|
|
this.controls.keyPanSpeed = 35
|
|
this.controls.keyRotateFactor = 10
|
|
this.controls.listenToKeyEvents(this.canvas as unknown as HTMLElement)
|
|
// Add lighting
|
|
const gridSizeX = parseInt(this.settings.gridSizeX ?? '10', 10)
|
|
const gridSizeZ = parseInt(this.settings.gridSizeZ ?? '10', 10)
|
|
this.scene.add(new THREE.HemisphereLight(0xffffff, 0xcccccc))
|
|
const light = new THREE.DirectionalLight()
|
|
light.position.set(gridSizeX, 50, gridSizeZ)
|
|
this.scene.add(light)
|
|
const light2 = new THREE.DirectionalLight()
|
|
light2.position.set(-gridSizeX, 50, gridSizeZ)
|
|
this.scene.add(light2)
|
|
// Set up render passes
|
|
this.composer = new EffectComposer(this.renderer)
|
|
this.composer.addPass(new RenderPass(this.scene, this.camera))
|
|
if (this.settings.renderStyle === 'shadowed') {
|
|
const ssaoPass = new SSAOPass(this.scene, this.camera)
|
|
ssaoPass.output = SSAOPass.OUTPUT.Default
|
|
ssaoPass.minDistance = 0
|
|
ssaoPass.maxDistance = 50
|
|
this.composer.addPass(ssaoPass)
|
|
}
|
|
this.composer.addPass(new OutputPass())
|
|
}
|
|
|
|
async load (cbLoadComplete, cbLoadProgress, cbLoadError): Promise<void> {
|
|
// Store callbacks
|
|
this.cbLoadComplete = cbLoadComplete
|
|
this.cbLoadProgress = cbLoadProgress
|
|
this.cbLoadError = cbLoadError
|
|
// Load
|
|
const LoaderClass: THREE.Loader | null = loaders[this.settings.format as string]
|
|
if (LoaderClass !== null) {
|
|
// Create loader
|
|
const loader = new LoaderClass()
|
|
// Set up DRACO loader for GLTF
|
|
if (LoaderClass === GLTFLoader) {
|
|
const dracoLoader = new DRACOLoader()
|
|
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.7/')
|
|
loader.setDRACOLoader(dracoLoader)
|
|
} else if (LoaderClass === DRACOLoader) {
|
|
loader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.7/')
|
|
} else if (LoaderClass === LDrawLoader) {
|
|
loader.setConditionalLineMaterial(LDrawConditionalLineMaterial);
|
|
loader.setPartsLibraryPath('https://raw.githubusercontent.com/gkjohnson/ldraw-parts-library/master/complete/ldraw/');
|
|
await loader.preloadMaterials('https://raw.githubusercontent.com/gkjohnson/ldraw-parts-library/master/colors/ldcfgalt.ldr');
|
|
}
|
|
// Load
|
|
loader.load(
|
|
this.settings.previewUrl ?? '',
|
|
this.onLoad.bind(this),
|
|
this.onLoadProgress.bind(this),
|
|
this.onLoadError.bind(this)
|
|
)
|
|
}
|
|
}
|
|
|
|
onLoadProgress (xhr): void {
|
|
const percentage = Math.floor((xhr.loaded / xhr.total) * 100)
|
|
this.cbLoadProgress(percentage)
|
|
}
|
|
|
|
onLoad (model): void {
|
|
let material = new THREE.MeshLambertMaterial({
|
|
flatShading: true,
|
|
color: (this.settings.objectColour ?? '#cccccc')
|
|
})
|
|
|
|
switch (this.settings.renderStyle) {
|
|
case 'normals':
|
|
material = new THREE.MeshNormalMaterial({
|
|
flatShading: true
|
|
})
|
|
break
|
|
}
|
|
// find mesh
|
|
let object: THREE.Mesh | THREE.Group | null = null
|
|
if (model.type === 'BufferGeometry') {
|
|
object = new THREE.Mesh(model, material)
|
|
} else if ('scene' in model) {
|
|
object = model.scene
|
|
} else {
|
|
object = model
|
|
}
|
|
// Set material
|
|
if (object == null) { return }
|
|
const overwriteMaterials = this.settings.renderStyle !== 'original'
|
|
object.traverse(function (node: THREE.Mesh) {
|
|
if (node.isMesh === true) {
|
|
if (node.material == null || overwriteMaterials) {
|
|
node.material = material
|
|
}
|
|
}
|
|
})
|
|
// Transform to screen coords from print
|
|
if (this.settings.yUp !== 'true') {
|
|
const coordSystemTransform = new THREE.Matrix4()
|
|
coordSystemTransform.set(
|
|
1,
|
|
0,
|
|
0,
|
|
0, // x -> x
|
|
0,
|
|
0,
|
|
1,
|
|
0, // z -> y
|
|
0,
|
|
-1,
|
|
0,
|
|
0, // y -> -z
|
|
0,
|
|
0,
|
|
0,
|
|
1
|
|
)
|
|
object.applyMatrix4(coordSystemTransform)
|
|
}
|
|
// Calculate bounding volumes
|
|
const bbox = new THREE.Box3().setFromObject(object)
|
|
const centre = new THREE.Vector3()
|
|
bbox.getCenter(centre)
|
|
const bsphere = new THREE.Sphere()
|
|
bbox.getBoundingSphere(bsphere)
|
|
const modelHeight = bbox.max.y - bbox.min.y
|
|
// Centre the model
|
|
object.position.set(-centre.x, -bbox.min.y, -centre.z)
|
|
|
|
// Configure camera
|
|
this.camera.position.z = this.camera.position.x = bsphere.radius * 1.63
|
|
this.camera.position.y = bsphere.radius * 0.75
|
|
this.controls.target.set(0, modelHeight / 2, 0)
|
|
this.scene.add(object)
|
|
// Add the grid
|
|
if (this.settings.showGrid === 'true') {
|
|
const gridSizeX = parseInt(this.settings.gridSizeX ?? '10', 10)
|
|
// TODO: use grid size Z here, see #834
|
|
this.gridHelper = new THREE.GridHelper(
|
|
gridSizeX,
|
|
gridSizeX / 10,
|
|
'magenta',
|
|
'cyan'
|
|
)
|
|
this.scene.add(this.gridHelper)
|
|
}
|
|
|
|
// Let's go!
|
|
this.ready = true
|
|
this.render()
|
|
|
|
// Listen for control updates and re-render
|
|
this.controls.addEventListener('change', this.render.bind(this))
|
|
|
|
// Report load complete
|
|
this.cbLoadComplete()
|
|
}
|
|
|
|
onLoadError (e): void {
|
|
console.log(e)
|
|
this.cbLoadError()
|
|
}
|
|
|
|
onResize (width, height, pixelRatio): void {
|
|
this.canvas.resize(width, height)
|
|
this.renderer.setSize(width, height, false)
|
|
this.composer.setSize(width, height, false)
|
|
// Update camera
|
|
this.camera.aspect = width / height
|
|
this.camera.updateProjectionMatrix()
|
|
// Update pixel ratio
|
|
this.renderer.setPixelRatio(pixelRatio)
|
|
this.composer.setPixelRatio(pixelRatio)
|
|
// Render!
|
|
this.render()
|
|
}
|
|
|
|
render (): void {
|
|
setTimeout(() => {
|
|
requestAnimationFrame(this.onAnimationFrame.bind(this))
|
|
}, 16) // ms per 60fps frame, near enough
|
|
}
|
|
|
|
onAnimationFrame (): void {
|
|
if (!this.ready || this.canvas === null || this.renderer === null) {
|
|
return
|
|
}
|
|
// Update controls to allow animation of damping
|
|
this.controls.update()
|
|
// Render
|
|
this.composer.render()
|
|
}
|
|
|
|
cleanup (): void {
|
|
if (typeof this.scene !== 'undefined' && this.scene !== null) {
|
|
this.scene.traverse(function (node) {
|
|
if (node instanceof THREE.Mesh) {
|
|
node.geometry.dispose()
|
|
node.material.dispose()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
Comlink.expose(OffscreenRenderer)
|