🏓 Adds scene builder to simplify three scene creation
This commit is contained in:
committed by
Rune Daugaard Harlyk
parent
2f070bed5d
commit
10efa9eeeb
@@ -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]}>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user