🏓 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">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import {
|
import { CanvasTexture, CircleGeometry, Mesh, MeshBasicMaterial} from 'three';
|
||||||
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 { dataBuffer, servoBuffer } from '../../lib/socket'
|
import { dataBuffer, servoBuffer } from '../../lib/socket'
|
||||||
import { lerp } from '../../lib/utils';
|
import { lerp } from '../../lib/utils';
|
||||||
import uzip from 'uzip';
|
import uzip from 'uzip';
|
||||||
@@ -32,9 +8,10 @@ import { outControllerData } from '../../lib/store';
|
|||||||
import Kinematic from '../../lib/kinematic';
|
import Kinematic from '../../lib/kinematic';
|
||||||
import location from '../../lib/location';
|
import location from '../../lib/location';
|
||||||
import FileCache from '../../lib/cache';
|
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 context: CanvasRenderingContext2D, texture: CanvasTexture
|
||||||
|
|
||||||
let modelAngles:number[] | Int16Array = new Array(12).fill(0)
|
let modelAngles:number[] | Int16Array = new Array(12).fill(0)
|
||||||
@@ -138,116 +115,51 @@ const cacheModelFiles = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadModel = () => {
|
const createScene = () => {
|
||||||
const url = '/spot_micro.urdf.xacro';
|
sceneManager = new SceneBuilder()
|
||||||
const xacroLoader = new XacroLoader();
|
.addRenderer({ antialias: true, canvas: canvas, alpha: true})
|
||||||
xacroLoader.load( url, xml => {
|
.addPerspectiveCamera({x:-0.5, y:0.5, z:1})
|
||||||
const urdfLoader = new URDFLoader();
|
.addOrbitControls(10, 30)
|
||||||
urdfLoader.workingPath = LoaderUtils.extractUrlBase( url );
|
.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 );
|
addVideoStream()
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createScene = () => {
|
const addVideoStream = () => {
|
||||||
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);
|
|
||||||
|
|
||||||
context = streamCanvas.getContext("2d");
|
context = streamCanvas.getContext("2d");
|
||||||
texture = new CanvasTexture( stream );
|
texture = new CanvasTexture( stream );
|
||||||
const liveStream = new Mesh( new CircleGeometry(35, 32), new MeshBasicMaterial({ map: texture }))
|
const liveStream = new Mesh( new CircleGeometry(35, 32), new MeshBasicMaterial({ map: texture }))
|
||||||
liveStream.position.z = -50
|
liveStream.position.z = -50
|
||||||
liveStream.visible = showStream
|
liveStream.visible = showStream
|
||||||
scene.add(liveStream)
|
sceneManager.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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleVideoStream = () => {
|
const handleVideoStream = () => {
|
||||||
if(!isLoaded || !showStream) return
|
if(!showStream) return
|
||||||
context.drawImage(stream, 0, 0)
|
context.drawImage(stream, 0, 0)
|
||||||
texture.needsUpdate = true;
|
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 = () => {
|
const render = () => {
|
||||||
requestAnimationFrame(render);
|
const robot = sceneManager.model
|
||||||
renderer.render(scene, camera);
|
|
||||||
|
|
||||||
if(!robot) return
|
if(!robot) return
|
||||||
|
|
||||||
handleRobotShadow()
|
sceneManager.model.rotation.z = lerp(robot.rotation.z, degToRad($dataBuffer[1] + 90), 0.1)
|
||||||
|
|
||||||
handleVideoStream()
|
handleVideoStream()
|
||||||
|
|
||||||
for (let i = 0; i < servoNames.length; i++) {
|
for (let i = 0; i < servoNames.length; i++) {
|
||||||
modelAngles[i] = lerp(robot.joints[servoNames[i]].angle * (180/Math.PI), modelTargetAngles[i], 0.1)
|
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)
|
modelBodyAngles.omega = lerp(robot.rotation.x * (180/Math.PI), modelTargeBodyAngles.omega - 90, 0.1)
|
||||||
@@ -256,7 +168,7 @@ const render = () => {
|
|||||||
}
|
}
|
||||||
</script>
|
</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">
|
<div class="absolute top-0 z-10 left-0 m-10">
|
||||||
<h1 class="text-on-background text-xl mb-2">Poses</h1>
|
<h1 class="text-on-background text-xl mb-2">Poses</h1>
|
||||||
@@ -267,7 +179,7 @@ const render = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<h1 class="text-on-background text-xl mt-4">Motor angles</h1>
|
<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">
|
<div class="flex justify-between mb-2">
|
||||||
<span class="w-40">{name}: </span>
|
<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]}>
|
<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