🏓 Adds scene builder to simplify three scene creation

This commit is contained in:
Rune Harlyk
2023-08-09 21:55:37 +02:00
committed by Rune Daugaard Harlyk
parent 2f070bed5d
commit 10efa9eeeb
2 changed files with 229 additions and 118 deletions
+30 -118
View File
@@ -1,30 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import {
WebGLRenderer,
PerspectiveCamera,
Scene,
Mesh,
PlaneGeometry,
ShadowMaterial,
DirectionalLight,
PCFSoftShadowMap,
sRGBEncoding,
AmbientLight,
MathUtils,
LoaderUtils,
GridHelper,
Camera,
FogExp2,
MeshBasicMaterial,
CanvasTexture,
CircleGeometry,
Object3D,
type Event
} from 'three';
import { XacroLoader } from 'xacro-parser';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import URDFLoader from 'urdf-loader';
import { CanvasTexture, CircleGeometry, Mesh, MeshBasicMaterial} from 'three';
import { dataBuffer, servoBuffer } from '../../lib/socket'
import { lerp } from '../../lib/utils';
import uzip from 'uzip';
@@ -32,16 +8,17 @@ import { outControllerData } from '../../lib/store';
import Kinematic from '../../lib/kinematic';
import location from '../../lib/location';
import FileCache from '../../lib/cache';
import SceneBuilder from './sceneBuilder';
let canvas: HTMLCanvasElement, streamCanvas: HTMLCanvasElement, stream: HTMLImageElement, scene: Scene, camera: Camera, renderer: WebGLRenderer, controls: OrbitControls, robot: Object3D<Event>, isLoaded = false;
let sceneManager:SceneBuilder
let canvas: HTMLCanvasElement, streamCanvas: HTMLCanvasElement, stream: HTMLImageElement
let context: CanvasRenderingContext2D, texture: CanvasTexture
let modelAngles:number[] | Int16Array = new Array(12).fill(0)
let modelTargetAngles:number[] | Int16Array = new Array(12).fill(0)
let modelBodyAngles:EulerAngle = {omega: 0, phi: 0, psi: 0 }
let modelTargeBodyAngles:EulerAngle = {omega: 0, phi: 0, psi: 0 }
let modelBodyAngles:EulerAngle = { omega: 0, phi: 0, psi: 0 }
let modelTargeBodyAngles:EulerAngle = { omega: 0, phi: 0, psi: 0 }
let modelBodyPoint:Point = {x: 0, y: 0, z: 0 }
let modelTargetBodyPoint:Point = {x: 0, y: 0, z: 0 }
@@ -138,116 +115,51 @@ const cacheModelFiles = async () => {
}
}
const loadModel = () => {
const url = '/spot_micro.urdf.xacro';
const xacroLoader = new XacroLoader();
xacroLoader.load( url, xml => {
const urdfLoader = new URDFLoader();
urdfLoader.workingPath = LoaderUtils.extractUrlBase( url );
const createScene = () => {
sceneManager = new SceneBuilder()
.addRenderer({ antialias: true, canvas: canvas, alpha: true})
.addPerspectiveCamera({x:-0.5, y:0.5, z:1})
.addOrbitControls(10, 30)
.addGroundPlane({x:0, y:-2, z:0})
.addGridHelper({size:250, divisions:125, y:-2})
.addAmbientLight({color:0xffffff, intensity:0.3})
.addDirectionalLight({x:50, y:100, z:100, color:0xffffff, intensity:0.9})
.addArrowHelper({origin:{x:0, y:0, z:0}, direction:{x:0, y:-2, z:0}})
.addFogExp2(0xcccccc, 0.015)
.loadModel('/spot_micro.urdf.xacro')
.handleResize()
.addRenderCb(render)
.startRenderLoop()
robot = urdfLoader.parse( xml );
robot.rotation.x = -Math.PI / 2;
robot.rotation.z = Math.PI / 2;
robot.traverse(c => c.castShadow = true);
robot.updateMatrixWorld(true);
robot.scale.setScalar(10);
scene.add( robot );
}, (error) => console.log(error));
addVideoStream()
}
const createScene = () => {
scene = new Scene();
camera = new PerspectiveCamera();
camera.position.set(-0.5, 0.5, 1);
renderer = new WebGLRenderer({ antialias: true, canvas: canvas, alpha: true });
renderer.outputEncoding = sRGBEncoding;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
const directionalLight = new DirectionalLight(0xffffff, 0.9);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.setScalar(2048);
directionalLight.shadow.mapSize.width = 1024;
directionalLight.shadow.mapSize.height = 1024;
directionalLight.position.set(50, 100, 100);
directionalLight.shadow.radius = 5
scene.add(directionalLight);
const ambientLight = new AmbientLight(0xffffff, 0.3);
scene.add(ambientLight);
if(!showStream) scene.fog = new FogExp2( 0xcccccc, 0.015 );
const ground = new Mesh( new PlaneGeometry(), new ShadowMaterial({side: 2}));
ground.rotation.x = -Math.PI / 2;
ground.scale.setScalar(30);
ground.position.y = -2
ground.receiveShadow = true;
scene.add(ground);
const addVideoStream = () => {
context = streamCanvas.getContext("2d");
texture = new CanvasTexture( stream );
const liveStream = new Mesh( new CircleGeometry(35, 32), new MeshBasicMaterial({ map: texture }))
liveStream.position.z = -50
liveStream.visible = showStream
scene.add(liveStream)
const gridHelper = new GridHelper(250, 125);
gridHelper.position.y = -2;
scene.add(gridHelper);
controls = new OrbitControls(camera, renderer.domElement);
controls.minDistance = 10;
controls.maxDistance = 30;
controls.update();
loadModel()
handleResize()
render()
}
const handleResize = () => {
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
sceneManager.scene.add(liveStream)
}
const handleVideoStream = () => {
if(!isLoaded || !showStream) return
if(!showStream) return
context.drawImage(stream, 0, 0)
texture.needsUpdate = true;
}
const handleRobotShadow = () => {
if(isLoaded) return
const intervalId = setInterval(() => {
robot.traverse(c => c.castShadow = true);
}, 10);
setTimeout(() => {
clearInterval(intervalId)
}, 1000);
isLoaded = true;
}
const render = () => {
requestAnimationFrame(render);
renderer.render(scene, camera);
const robot = sceneManager.model
if(!robot) return
handleRobotShadow()
sceneManager.model.rotation.z = lerp(robot.rotation.z, degToRad($dataBuffer[1] + 90), 0.1)
handleVideoStream()
for (let i = 0; i < servoNames.length; i++) {
modelAngles[i] = lerp(robot.joints[servoNames[i]].angle * (180/Math.PI), modelTargetAngles[i], 0.1)
robot.joints[servoNames[i]].setJointValue(MathUtils.degToRad(modelAngles[i]));
robot.joints[servoNames[i]].setJointValue(degToRad(modelAngles[i]));
}
modelBodyAngles.omega = lerp(robot.rotation.x * (180/Math.PI), modelTargeBodyAngles.omega - 90, 0.1)
@@ -256,7 +168,7 @@ const render = () => {
}
</script>
<svelte:window on:resize={handleResize}></svelte:window>
<svelte:window on:resize={sceneManager.handleResize}></svelte:window>
<div class="absolute top-0 z-10 left-0 m-10">
<h1 class="text-on-background text-xl mb-2">Poses</h1>
@@ -267,7 +179,7 @@ const render = () => {
</div>
<div class="w-full">
<h1 class="text-on-background text-xl mt-4">Motor angles</h1>
{#each Object.entries(robot?.joints ?? {}).filter(x => x[1].jointValue.length > 0) as [name, joint], i}
{#each Object.entries(sceneManager?.model?.joints ?? {}).filter(x => x[1].jointValue.length > 0) as [name, joint], i}
<div class="flex justify-between mb-2">
<span class="w-40">{name}: </span>
<input type="range" min="{radToDeg(joint.limit.lower)}" max="{radToDeg(joint.limit.upper)}" step="0.1" class="accent-primary" bind:value={$servoBuffer[i]}>
+199
View File
@@ -0,0 +1,199 @@
import { Mesh,
PerspectiveCamera,
PlaneGeometry,
Scene,
ShadowMaterial,
WebGLRenderer,
AmbientLight,
DirectionalLight,
PCFSoftShadowMap,
GridHelper,
ArrowHelper,
Vector3,
LoaderUtils,
Object3D,
FogExp2,
CanvasTexture,
type ColorRepresentation,
type WebGLRendererParameters,
} from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import URDFLoader from "urdf-loader";
import { XacroLoader } from "xacro-parser";
export const addScene = () => new Scene()
interface position {
x?: number,
y?: number,
z?: number
}
interface light {
color?: ColorRepresentation,
intensity?: number
}
interface gridOptions {
divisions?: number,
size?: number,
}
interface arrowOptions {
origin:position,
direction:position,
length?:number,
color?:ColorRepresentation
}
type directionalLight = position & light
type gridHelperOptions = gridOptions & position
export default class SceneBuilder {
public scene: Scene
public camera: PerspectiveCamera
public ground: Mesh
public renderer:WebGLRenderer
public controls:OrbitControls
public callback:Function
public gridHelper: GridHelper;
public model: Object3D<Event>
public liveStreamTexture: CanvasTexture
private fog:FogExp2
private isLoaded:boolean = false
constructor() {
this.scene = new Scene()
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;
document.body.appendChild(this.renderer.domElement);
return this
}
public addPerspectiveCamera = (options:position) => {
this.camera = new PerspectiveCamera();
this.camera.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0);
this.scene.add(this.camera);
return this
}
public addGroundPlane = (options:position) => {
this.ground = new Mesh( new PlaneGeometry(), new ShadowMaterial({side: 2}));
this.ground.rotation.x = -Math.PI / 2;
this.ground.scale.setScalar(30);
this.ground.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0);
this.ground.receiveShadow = true;
this.scene.add(this.ground);
return this
}
public addOrbitControls = (minDistance:number, maxDistance:number) => {
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.minDistance = minDistance;
this.controls.maxDistance = maxDistance;
this.controls.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.mapSize.setScalar(2048);
directionalLight.shadow.mapSize.width = 1024;
directionalLight.shadow.mapSize.height = 1024;
directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0);
directionalLight.shadow.radius = 5
this.scene.add(directionalLight);
return this
}
public addGridHelper = (options:gridHelperOptions) => {
this.gridHelper = new GridHelper(options.size, options.divisions);
this.gridHelper.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0);
this.scene.add(this.gridHelper);
return this
}
public addFogExp2 = (color:ColorRepresentation, density?:number) => {
this.scene.fog = new FogExp2(color, density);
return this
}
public handleResize = () => {
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(window.devicePixelRatio);
this.camera.aspect = window.innerWidth / window.innerHeight;
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.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
}
public loadModel = (urlXacro:string) => {
const xacroLoader = new XacroLoader();
xacroLoader.load(urlXacro, xml => {
const urdfLoader = new URDFLoader();
urdfLoader.workingPath = LoaderUtils.extractUrlBase(urlXacro);
this.model = urdfLoader.parse(xml);
this.model.rotation.x = -Math.PI / 2;
this.model.rotation.z = Math.PI / 2;
this.model.traverse(c => c.castShadow = true);
this.model.updateMatrixWorld(true);
this.model.scale.setScalar(10);
this.scene.add(this.model);
}, (error) => console.log(error));
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;
}
}