Enables better zoom for viz

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