diff --git a/app/src/lib/components/Visualization.svelte b/app/src/lib/components/Visualization.svelte index 8ae73a7..f48c9d1 100644 --- a/app/src/lib/components/Visualization.svelte +++ b/app/src/lib/components/Visualization.svelte @@ -177,7 +177,7 @@ sceneManager .addRenderer({ antialias: true, canvas, alpha: true }) .addPerspectiveCamera({ x: -0.5, y: 0.5, z: 1 }) - .addOrbitControls(8, 30, orbit) + .addOrbitControls(2, 20, orbit) .addDirectionalLight({ x: 10, y: 20, z: 10, color: 0xffffff, intensity: 3 }) .addAmbientLight({ color: 0xffffff, intensity: 0.5 }) .addFogExp2(0xcccccc, 0.015) diff --git a/app/src/lib/sceneBuilder.ts b/app/src/lib/sceneBuilder.ts index 5f9f510..43648b2 100644 --- a/app/src/lib/sceneBuilder.ts +++ b/app/src/lib/sceneBuilder.ts @@ -1,379 +1,380 @@ import { - Mesh, - PerspectiveCamera, - PlaneGeometry, - Scene, - WebGLRenderer, - AmbientLight, - DirectionalLight, - PCFSoftShadowMap, - type GridHelper, - ArrowHelper, - Vector3, - FogExp2, - CanvasTexture, - type ColorRepresentation, - type WebGLRendererParameters, - MeshPhongMaterial, - EquirectangularReflectionMapping, - ACESFilmicToneMapping, - MathUtils, - Group, - MeshBasicMaterial, - RepeatWrapping -} from 'three'; -import { Sky } from 'three/addons/objects/Sky.js'; -import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; -import { TransformControls } from 'three/examples/jsm/controls/TransformControls'; -import { Reflector } from 'three/examples/jsm/objects/Reflector.js'; -import { type URDFJoint, type URDFMimicJoint, type URDFRobot } from 'urdf-loader'; -import { PointerURDFDragControls } from 'urdf-loader/src/URDFDragControls'; -import { sunCalculator } from './utilities/position-utilities'; + Mesh, + PerspectiveCamera, + PlaneGeometry, + Scene, + WebGLRenderer, + AmbientLight, + DirectionalLight, + PCFSoftShadowMap, + type GridHelper, + ArrowHelper, + Vector3, + FogExp2, + CanvasTexture, + type ColorRepresentation, + type WebGLRendererParameters, + MeshPhongMaterial, + EquirectangularReflectionMapping, + ACESFilmicToneMapping, + MathUtils, + Group, + MeshBasicMaterial, + RepeatWrapping +} from 'three' +import { Sky } from 'three/addons/objects/Sky.js' +import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' +import { TransformControls } from 'three/examples/jsm/controls/TransformControls' +import { Reflector } from 'three/examples/jsm/objects/Reflector.js' +import { type URDFJoint, type URDFMimicJoint, type URDFRobot } from 'urdf-loader' +import { PointerURDFDragControls } from 'urdf-loader/src/URDFDragControls' +import { sunCalculator } from './utilities/position-utilities' -export const addScene = () => new Scene(); +export const addScene = () => new Scene() interface position { - x?: number; - y?: number; - z?: number; + x?: number + y?: number + z?: number } interface light { - color?: ColorRepresentation; - intensity?: number; + color?: ColorRepresentation + intensity?: number } interface arrowOptions { - origin: position; - direction: position; - length?: number; - color?: ColorRepresentation; + origin: position + direction: position + length?: number + color?: ColorRepresentation } -type directionalLight = position & light; +type directionalLight = position & light export default class SceneBuilder { - public scene: Scene; - public camera!: PerspectiveCamera; - public ground!: Mesh; - public renderer!: WebGLRenderer; - public orbit: OrbitControls; - public callback: Function | undefined; - public gridHelper!: GridHelper; - public model!: URDFRobot; - public liveStreamTexture!: CanvasTexture; - private fog!: FogExp2; - private isLoaded: boolean = false; - public isDragging: boolean = false; - highlightMaterial: any; - sky!: Sky; - transformControl: TransformControls; - public modelGroup!: Group; + public scene: Scene + public camera!: PerspectiveCamera + public ground!: Mesh + public renderer!: WebGLRenderer + public orbit: OrbitControls + public callback: Function | undefined + public gridHelper!: GridHelper + public model!: URDFRobot + public liveStreamTexture!: CanvasTexture + private fog!: FogExp2 + private isLoaded: boolean = false + public isDragging: boolean = false + highlightMaterial: any + sky!: Sky + transformControl: TransformControls + public modelGroup!: Group - constructor() { - this.scene = new Scene(); - if (this.scene.environment?.mapping) { - this.scene.environment.mapping = EquirectangularReflectionMapping; - } - return this; + constructor() { + this.scene = new Scene() + if (this.scene.environment?.mapping) { + this.scene.environment.mapping = EquirectangularReflectionMapping + } + return this + } + + public addRenderer = (parameters?: WebGLRendererParameters) => { + this.renderer = new WebGLRenderer(parameters) + this.renderer.outputColorSpace = 'srgb' + this.renderer.shadowMap.enabled = true + this.renderer.shadowMap.type = PCFSoftShadowMap + this.renderer.toneMapping = ACESFilmicToneMapping + this.renderer.toneMappingExposure = 0.85 + if (!parameters?.canvas) document.body.appendChild(this.renderer.domElement) + return this + } + + public addSky = () => { + this.sky = new Sky() + this.sky.scale.setScalar(450000) + this.scene.add(this.sky) + const effectController = { + turbidity: 10, + rayleigh: 3, + mieCoefficient: 0.005, + mieDirectionalG: 0.7, + elevation: sunCalculator.calculateSunElevation(), + azimuth: 200, + exposure: this.renderer.toneMappingExposure + } + const uniforms = this.sky.material.uniforms + uniforms['turbidity'].value = effectController.turbidity + uniforms['rayleigh'].value = effectController.rayleigh + uniforms['mieCoefficient'].value = effectController.mieCoefficient + uniforms['mieDirectionalG'].value = effectController.mieDirectionalG + this.renderer.toneMappingExposure = 0.5 + const phi = MathUtils.degToRad(90 - effectController.elevation) + const theta = MathUtils.degToRad(effectController.azimuth) + const sun = new Vector3() + + sun.setFromSphericalCoords(1, phi, theta) + uniforms['sunPosition'].value.copy(sun) + return this + } + + public addPerspectiveCamera = (options: position) => { + this.camera = new PerspectiveCamera() + this.camera.position.set(options.x ?? 0, options.y ?? 2.7, options.z ?? 0) + this.scene.add(this.camera) + return this + } + + public addGroundPlane = (options?: position) => { + const checkerboardTexture = this.createCheckerboardTexture(1024, 2) + checkerboardTexture.wrapS = RepeatWrapping + checkerboardTexture.wrapT = RepeatWrapping + checkerboardTexture.repeat.set(100, 100) + const checkerboardMat = new MeshBasicMaterial({ + map: checkerboardTexture, + opacity: 0.1, + transparent: true + }) + + const plane = new PlaneGeometry(400, 400) + + this.ground = new Mesh(plane, checkerboardMat) + this.ground.rotation.x = -Math.PI / 2 + this.ground.position.set(options?.x ?? 0, options?.y ?? 0.01, options?.z ?? 0) + this.ground.receiveShadow = true + this.scene.add(this.ground) + + const mirror = new Reflector(plane, { + clipBias: 0.003, + textureWidth: window.innerWidth * window.devicePixelRatio, + textureHeight: window.innerHeight * window.devicePixelRatio, + color: 0x00bfff + }) + mirror.rotateX(-Math.PI / 2) + this.scene.add(mirror) + + return this + } + + public addOrbitControls = (minDistance: number, maxDistance: number, autoRotate = true) => { + this.orbit = new OrbitControls(this.camera, this.renderer.domElement) + this.orbit.minDistance = minDistance + (maxDistance - minDistance) / 2 + this.orbit.maxDistance = maxDistance + this.orbit.autoRotate = autoRotate + this.orbit.update() + this.orbit.minDistance = minDistance + return this + } + + public addAmbientLight = (options: light) => { + const ambientLight = new AmbientLight(options.color, options.intensity) + this.scene.add(ambientLight) + return this + } + + public addDirectionalLight = (options: directionalLight) => { + const directionalLight = new DirectionalLight(options.color, options.intensity) + directionalLight.castShadow = true + directionalLight.shadow.camera.top = 10 + directionalLight.shadow.camera.bottom = -10 + directionalLight.shadow.camera.right = 10 + directionalLight.shadow.camera.left = -10 + directionalLight.shadow.mapSize.set(4096, 4096) + + directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0) + this.scene.add(directionalLight) + return this + } + + private createCheckerboardTexture = (size: number, squares: number) => { + const canvas = document.createElement('canvas') + canvas.width = size + canvas.height = size + const context = canvas.getContext('2d') + + const squareSize = size / squares + + for (let y = 0; y < squares; y++) { + for (let x = 0; x < squares; x++) { + context!.fillStyle = (x + y) % 2 === 0 ? '#ffffff' : '#000000' + context!.fillRect(x * squareSize, y * squareSize, squareSize, squareSize) + } } - public addRenderer = (parameters?: WebGLRendererParameters) => { - this.renderer = new WebGLRenderer(parameters); - this.renderer.outputColorSpace = 'srgb'; - this.renderer.shadowMap.enabled = true; - this.renderer.shadowMap.type = PCFSoftShadowMap; - this.renderer.toneMapping = ACESFilmicToneMapping; - this.renderer.toneMappingExposure = 0.85; - if (!parameters?.canvas) document.body.appendChild(this.renderer.domElement); - return this; - }; + const texture = new CanvasTexture(canvas) + texture.wrapS = texture.wrapT = RepeatWrapping + texture.anisotropy = 16 + return texture + } - public addSky = () => { - this.sky = new Sky(); - this.sky.scale.setScalar(450000); - this.scene.add(this.sky); - const effectController = { - turbidity: 10, - rayleigh: 3, - mieCoefficient: 0.005, - mieDirectionalG: 0.7, - elevation: sunCalculator.calculateSunElevation(), - azimuth: 200, - exposure: this.renderer.toneMappingExposure - }; - const uniforms = this.sky.material.uniforms; - uniforms['turbidity'].value = effectController.turbidity; - uniforms['rayleigh'].value = effectController.rayleigh; - uniforms['mieCoefficient'].value = effectController.mieCoefficient; - uniforms['mieDirectionalG'].value = effectController.mieDirectionalG; - this.renderer.toneMappingExposure = 0.5; - const phi = MathUtils.degToRad(90 - effectController.elevation); - const theta = MathUtils.degToRad(effectController.azimuth); - const sun = new Vector3(); + public addFogExp2 = (color: ColorRepresentation, density?: number) => { + this.scene.fog = new FogExp2(color, density) + return this + } - sun.setFromSphericalCoords(1, phi, theta); - uniforms['sunPosition'].value.copy(sun); - return this; - }; - - public addPerspectiveCamera = (options: position) => { - this.camera = new PerspectiveCamera(); - this.camera.position.set(options.x ?? 0, options.y ?? 2.7, options.z ?? 0); - this.scene.add(this.camera); - return this; - }; - - public addGroundPlane = (options?: position) => { - const checkerboardTexture = this.createCheckerboardTexture(1024, 2); - checkerboardTexture.wrapS = RepeatWrapping; - checkerboardTexture.wrapT = RepeatWrapping; - checkerboardTexture.repeat.set(100, 100); - const checkerboardMat = new MeshBasicMaterial({ - map: checkerboardTexture, - opacity: 0.1, - transparent: true - }); - - const plane = new PlaneGeometry(400, 400); - - this.ground = new Mesh(plane, checkerboardMat); - this.ground.rotation.x = -Math.PI / 2; - this.ground.position.set(options?.x ?? 0, options?.y ?? 0.01, options?.z ?? 0); - this.ground.receiveShadow = true; - this.scene.add(this.ground); - - const mirror = new Reflector(plane, { - clipBias: 0.003, - textureWidth: window.innerWidth * window.devicePixelRatio, - textureHeight: window.innerHeight * window.devicePixelRatio, - color: 0x00bfff - }); - mirror.rotateX(-Math.PI / 2); - this.scene.add(mirror); - - return this; - }; - - public addOrbitControls = (minDistance: number, maxDistance: number, autoRotate = true) => { - this.orbit = new OrbitControls(this.camera, this.renderer.domElement); - this.orbit.minDistance = minDistance; - this.orbit.maxDistance = maxDistance; - this.orbit.autoRotate = autoRotate; - this.orbit.update(); - return this; - }; - - public addAmbientLight = (options: light) => { - const ambientLight = new AmbientLight(options.color, options.intensity); - this.scene.add(ambientLight); - return this; - }; - - public addDirectionalLight = (options: directionalLight) => { - const directionalLight = new DirectionalLight(options.color, options.intensity); - directionalLight.castShadow = true; - directionalLight.shadow.camera.top = 10; - directionalLight.shadow.camera.bottom = -10; - directionalLight.shadow.camera.right = 10; - directionalLight.shadow.camera.left = -10; - directionalLight.shadow.mapSize.set(4096, 4096); - - directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0); - this.scene.add(directionalLight); - return this; - }; - - private createCheckerboardTexture = (size: number, squares: number) => { - const canvas = document.createElement('canvas'); - canvas.width = size; - canvas.height = size; - const context = canvas.getContext('2d'); - - const squareSize = size / squares; - - for (let y = 0; y < squares; y++) { - for (let x = 0; x < squares; x++) { - context!.fillStyle = (x + y) % 2 === 0 ? '#ffffff' : '#000000'; - context!.fillRect(x * squareSize, y * squareSize, squareSize, squareSize); - } - } - - const texture = new CanvasTexture(canvas); - texture.wrapS = texture.wrapT = RepeatWrapping; - texture.anisotropy = 16; - return texture; - }; - - public addFogExp2 = (color: ColorRepresentation, density?: number) => { - this.scene.fog = new FogExp2(color, density); - return this; - }; - - public fillParent = () => { - const parentElement = this.renderer.domElement.parentElement; - if (parentElement) { - const width = parentElement.clientWidth; - const height = parentElement.clientHeight; - this.handleResize(width, height); - } - return this; - }; - - public handleResize = (width = window.innerWidth, height = window.innerHeight) => { - this.renderer.setSize(width, height); - this.renderer.setPixelRatio(window.devicePixelRatio); - this.camera.aspect = width / height; - this.camera.updateProjectionMatrix(); - return this; - }; - - public addRenderCb = (callback: Function) => { - this.callback = callback; - return this; - }; - - public startRenderLoop = () => { - this.renderer.setAnimationLoop(() => { - this.renderer.render(this.scene, this.camera); - this.orbit.update(); - this.handleRobotShadow(); - if (this.callback) this.callback(); - if (!this.liveStreamTexture) return; - }); - return this; - }; - - public addArrowHelper = (options?: arrowOptions) => { - const dir = new Vector3( - options?.direction.x ?? 0, - options?.direction.y ?? 0, - options?.direction.z ?? 0 - ); - const origin = new Vector3( - options?.origin.x ?? 0, - options?.origin.y ?? 0, - options?.origin.z ?? 0 - ); - const arrowHelper = new ArrowHelper( - dir, - origin, - options?.length ?? 1.5, - options?.color ?? 0xff0000 - ); - this.scene.add(arrowHelper); - return this; - }; - - private setJointValue(jointName: string, angle: number) { - if (!this.model) return; - if (!this.model.joints[jointName]) return; - this.model.joints[jointName].setJointValue(angle); + public fillParent = () => { + const parentElement = this.renderer.domElement.parentElement + if (parentElement) { + const width = parentElement.clientWidth + const height = parentElement.clientHeight + this.handleResize(width, height) } + return this + } - isJoint = (j: URDFJoint) => j.isURDFJoint && j.jointType !== 'fixed'; + public handleResize = (width = window.innerWidth, height = window.innerHeight) => { + this.renderer.setSize(width, height) + this.renderer.setPixelRatio(window.devicePixelRatio) + this.camera.aspect = width / height + this.camera.updateProjectionMatrix() + return this + } - highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => { - const traverse = (c: any) => { - if (c.type === 'Mesh') { - if (revert) { - c.material = c.__origMaterial; - delete c.__origMaterial; - } else { - c.__origMaterial = c.material; - c.material = material; - } - } + public addRenderCb = (callback: Function) => { + this.callback = callback + return this + } - if (c === m || !this.isJoint(c)) { - for (let i = 0; i < c.children.length; i++) { - const child = c.children[i]; - if (!child.isURDFCollider) { - traverse(c.children[i]); - } - } - } - }; - traverse(m); - }; + public startRenderLoop = () => { + this.renderer.setAnimationLoop(() => { + this.renderer.render(this.scene, this.camera) + this.orbit.update() + this.handleRobotShadow() + if (this.callback) this.callback() + if (!this.liveStreamTexture) return + }) + return this + } - public addTransformControls = (model: any) => { - this.transformControl = new TransformControls(this.camera, this.renderer.domElement); - this.transformControl.addEventListener('dragging-changed', (event: any) => { - this.orbit.enabled = !event.value; - this.isDragging = !event.value; - }); - this.transformControl.attach(model); - this.scene.add(this.transformControl); - this.transformControl.setMode('rotate'); - return this; - }; + public addArrowHelper = (options?: arrowOptions) => { + const dir = new Vector3( + options?.direction.x ?? 0, + options?.direction.y ?? 0, + options?.direction.z ?? 0 + ) + const origin = new Vector3( + options?.origin.x ?? 0, + options?.origin.y ?? 0, + options?.origin.z ?? 0 + ) + const arrowHelper = new ArrowHelper( + dir, + origin, + options?.length ?? 1.5, + options?.color ?? 0xff0000 + ) + this.scene.add(arrowHelper) + return this + } - public addModel = (model: any) => { - this.modelGroup = new Group(); - this.modelGroup.add(model); - this.model = model; - this.scene.add(this.modelGroup); - return this; - }; + private setJointValue(jointName: string, angle: number) { + if (!this.model) return + if (!this.model.joints[jointName]) return + this.model.joints[jointName].setJointValue(angle) + } - public addDragControl = (updateAngle: any) => { - const highlightColor = '#FFFFFF'; - const highlightMaterial = new MeshPhongMaterial({ - shininess: 10, - color: highlightColor, - emissive: highlightColor, - emissiveIntensity: 0.9 - }); + isJoint = (j: URDFJoint) => j.isURDFJoint && j.jointType !== 'fixed' - const dragControls = new PointerURDFDragControls( - this.scene, - this.camera, - this.renderer.domElement - ); - dragControls.updateJoint = (joint: URDFMimicJoint, angle: number) => { - this.setJointValue(joint.name, angle); - updateAngle(joint.name, angle); - }; - dragControls.onDragStart = () => { - this.orbit.enabled = false; - this.isDragging = true; - }; - dragControls.onDragEnd = () => { - this.orbit.enabled = true; - this.isDragging = false; - }; - dragControls.onHover = (joint: URDFMimicJoint) => - this.highlightLinkGeometry(joint, false, highlightMaterial); - dragControls.onUnhover = (joint: URDFMimicJoint) => - this.highlightLinkGeometry(joint, true, highlightMaterial); + highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => { + const traverse = (c: any) => { + if (c.type === 'Mesh') { + if (revert) { + c.material = c.__origMaterial + delete c.__origMaterial + } else { + c.__origMaterial = c.material + c.material = material + } + } - this.renderer.domElement.addEventListener( - 'touchstart', - data => dragControls._mouseDown(data.touches[0]), - { passive: true } - ); - this.renderer.domElement.addEventListener( - 'touchmove', - data => dragControls._mouseMove(data.touches[0]), - { passive: true } - ); - this.renderer.domElement.addEventListener( - 'touchend', - data => dragControls._mouseUp(data.touches[0]), - { passive: true } - ); - return this; - }; + if (c === m || !this.isJoint(c)) { + for (let i = 0; i < c.children.length; i++) { + const child = c.children[i] + if (!child.isURDFCollider) { + traverse(c.children[i]) + } + } + } + } + traverse(m) + } - public toggleFog = () => { - this.scene.fog = this.scene.fog ? null : this.fog; - }; + public addTransformControls = (model: any) => { + this.transformControl = new TransformControls(this.camera, this.renderer.domElement) + this.transformControl.addEventListener('dragging-changed', (event: any) => { + this.orbit.enabled = !event.value + this.isDragging = !event.value + }) + this.transformControl.attach(model) + this.scene.add(this.transformControl) + this.transformControl.setMode('rotate') + return this + } - private handleRobotShadow = () => { - if (this.isLoaded) return; - const intervalId = setInterval(() => this.model?.traverse(c => (c.castShadow = true)), 10); - setTimeout(() => clearInterval(intervalId), 1000); - this.isLoaded = true; - }; + public addModel = (model: any) => { + this.modelGroup = new Group() + this.modelGroup.add(model) + this.model = model + this.scene.add(this.modelGroup) + return this + } + + public addDragControl = (updateAngle: any) => { + const highlightColor = '#FFFFFF' + const highlightMaterial = new MeshPhongMaterial({ + shininess: 10, + color: highlightColor, + emissive: highlightColor, + emissiveIntensity: 0.9 + }) + + const dragControls = new PointerURDFDragControls( + this.scene, + this.camera, + this.renderer.domElement + ) + dragControls.updateJoint = (joint: URDFMimicJoint, angle: number) => { + this.setJointValue(joint.name, angle) + updateAngle(joint.name, angle) + } + dragControls.onDragStart = () => { + this.orbit.enabled = false + this.isDragging = true + } + dragControls.onDragEnd = () => { + this.orbit.enabled = true + this.isDragging = false + } + dragControls.onHover = (joint: URDFMimicJoint) => + this.highlightLinkGeometry(joint, false, highlightMaterial) + dragControls.onUnhover = (joint: URDFMimicJoint) => + this.highlightLinkGeometry(joint, true, highlightMaterial) + + this.renderer.domElement.addEventListener( + 'touchstart', + data => dragControls._mouseDown(data.touches[0]), + { passive: true } + ) + this.renderer.domElement.addEventListener( + 'touchmove', + data => dragControls._mouseMove(data.touches[0]), + { passive: true } + ) + this.renderer.domElement.addEventListener( + 'touchend', + data => dragControls._mouseUp(data.touches[0]), + { passive: true } + ) + return this + } + + public toggleFog = () => { + this.scene.fog = this.scene.fog ? null : this.fog + } + + private handleRobotShadow = () => { + if (this.isLoaded) return + const intervalId = setInterval(() => this.model?.traverse(c => (c.castShadow = true)), 10) + setTimeout(() => clearInterval(intervalId), 1000) + this.isLoaded = true + } }