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)
+349 -348
View File
@@ -1,379 +1,380 @@
import { import {
Mesh, Mesh,
PerspectiveCamera, PerspectiveCamera,
PlaneGeometry, PlaneGeometry,
Scene, Scene,
WebGLRenderer, WebGLRenderer,
AmbientLight, AmbientLight,
DirectionalLight, DirectionalLight,
PCFSoftShadowMap, PCFSoftShadowMap,
type GridHelper, type GridHelper,
ArrowHelper, ArrowHelper,
Vector3, Vector3,
FogExp2, FogExp2,
CanvasTexture, CanvasTexture,
type ColorRepresentation, type ColorRepresentation,
type WebGLRendererParameters, type WebGLRendererParameters,
MeshPhongMaterial, MeshPhongMaterial,
EquirectangularReflectionMapping, EquirectangularReflectionMapping,
ACESFilmicToneMapping, ACESFilmicToneMapping,
MathUtils, MathUtils,
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) => {
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) => { const texture = new CanvasTexture(canvas)
this.renderer = new WebGLRenderer(parameters); texture.wrapS = texture.wrapT = RepeatWrapping
this.renderer.outputColorSpace = 'srgb'; texture.anisotropy = 16
this.renderer.shadowMap.enabled = true; return texture
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 = () => { public addFogExp2 = (color: ColorRepresentation, density?: number) => {
this.sky = new Sky(); this.scene.fog = new FogExp2(color, density)
this.sky.scale.setScalar(450000); return this
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); public fillParent = () => {
uniforms['sunPosition'].value.copy(sun); const parentElement = this.renderer.domElement.parentElement
return this; if (parentElement) {
}; const width = parentElement.clientWidth
const height = parentElement.clientHeight
public addPerspectiveCamera = (options: position) => { this.handleResize(width, height)
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);
} }
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) => { public addRenderCb = (callback: Function) => {
const traverse = (c: any) => { this.callback = callback
if (c.type === 'Mesh') { return this
if (revert) { }
c.material = c.__origMaterial;
delete c.__origMaterial;
} else {
c.__origMaterial = c.material;
c.material = material;
}
}
if (c === m || !this.isJoint(c)) { public startRenderLoop = () => {
for (let i = 0; i < c.children.length; i++) { this.renderer.setAnimationLoop(() => {
const child = c.children[i]; this.renderer.render(this.scene, this.camera)
if (!child.isURDFCollider) { this.orbit.update()
traverse(c.children[i]); this.handleRobotShadow()
} if (this.callback) this.callback()
} if (!this.liveStreamTexture) return
} })
}; return this
traverse(m); }
};
public addTransformControls = (model: any) => { public addArrowHelper = (options?: arrowOptions) => {
this.transformControl = new TransformControls(this.camera, this.renderer.domElement); const dir = new Vector3(
this.transformControl.addEventListener('dragging-changed', (event: any) => { options?.direction.x ?? 0,
this.orbit.enabled = !event.value; options?.direction.y ?? 0,
this.isDragging = !event.value; options?.direction.z ?? 0
}); )
this.transformControl.attach(model); const origin = new Vector3(
this.scene.add(this.transformControl); options?.origin.x ?? 0,
this.transformControl.setMode('rotate'); options?.origin.y ?? 0,
return this; 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) => { private setJointValue(jointName: string, angle: number) {
this.modelGroup = new Group(); if (!this.model) return
this.modelGroup.add(model); if (!this.model.joints[jointName]) return
this.model = model; this.model.joints[jointName].setJointValue(angle)
this.scene.add(this.modelGroup); }
return this;
};
public addDragControl = (updateAngle: any) => { isJoint = (j: URDFJoint) => j.isURDFJoint && j.jointType !== 'fixed'
const highlightColor = '#FFFFFF';
const highlightMaterial = new MeshPhongMaterial({
shininess: 10,
color: highlightColor,
emissive: highlightColor,
emissiveIntensity: 0.9
});
const dragControls = new PointerURDFDragControls( highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => {
this.scene, const traverse = (c: any) => {
this.camera, if (c.type === 'Mesh') {
this.renderer.domElement if (revert) {
); c.material = c.__origMaterial
dragControls.updateJoint = (joint: URDFMimicJoint, angle: number) => { delete c.__origMaterial
this.setJointValue(joint.name, angle); } else {
updateAngle(joint.name, angle); c.__origMaterial = c.material
}; c.material = material
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( if (c === m || !this.isJoint(c)) {
'touchstart', for (let i = 0; i < c.children.length; i++) {
data => dragControls._mouseDown(data.touches[0]), const child = c.children[i]
{ passive: true } if (!child.isURDFCollider) {
); traverse(c.children[i])
this.renderer.domElement.addEventListener( }
'touchmove', }
data => dragControls._mouseMove(data.touches[0]), }
{ passive: true } }
); traverse(m)
this.renderer.domElement.addEventListener( }
'touchend',
data => dragControls._mouseUp(data.touches[0]),
{ passive: true }
);
return this;
};
public toggleFog = () => { public addTransformControls = (model: any) => {
this.scene.fog = this.scene.fog ? null : this.fog; 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 = () => { public addModel = (model: any) => {
if (this.isLoaded) return; this.modelGroup = new Group()
const intervalId = setInterval(() => this.model?.traverse(c => (c.castShadow = true)), 10); this.modelGroup.add(model)
setTimeout(() => clearInterval(intervalId), 1000); this.model = model
this.isLoaded = true; 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
}
} }