From 357f33bf5471fadc5e64a27557ea5273fe28faac Mon Sep 17 00:00:00 2001 From: Chris <cmbirmingham19@gmail.com> Date: Tue, 14 Nov 2023 12:58:14 -0800 Subject: [PATCH] Refactor Bindings --- src/Bindings/CameraBinding.ts | 28 ++ src/Bindings/Lightbinding.ts | 58 +++ src/Bindings/MaterialBinding.ts | 251 ++++++++++++ src/Bindings/MotorBindings.ts | 80 ++++ src/Bindings/ObjectBinding.ts | 169 ++++++++ src/{ => Bindings}/RobotBinding.ts | 473 +++++------------------ src/Bindings/RobotLink.ts | 160 ++++++++ src/{ => Bindings}/SceneBinding.ts | 597 +++-------------------------- src/Bindings/WeightBinding.ts | 44 +++ src/Bindings/helpers.ts | 16 + src/Sim.tsx | 2 +- src/robots/demobot.ts | 3 + 12 files changed, 953 insertions(+), 928 deletions(-) create mode 100644 src/Bindings/CameraBinding.ts create mode 100644 src/Bindings/Lightbinding.ts create mode 100644 src/Bindings/MaterialBinding.ts create mode 100644 src/Bindings/MotorBindings.ts create mode 100644 src/Bindings/ObjectBinding.ts rename src/{ => Bindings}/RobotBinding.ts (73%) create mode 100644 src/Bindings/RobotLink.ts rename src/{ => Bindings}/SceneBinding.ts (63%) create mode 100644 src/Bindings/WeightBinding.ts create mode 100644 src/Bindings/helpers.ts diff --git a/src/Bindings/CameraBinding.ts b/src/Bindings/CameraBinding.ts new file mode 100644 index 00000000..f97128d8 --- /dev/null +++ b/src/Bindings/CameraBinding.ts @@ -0,0 +1,28 @@ +import { ArcRotateCamera, Scene as babylScene, Camera as babylCamera } from '@babylonjs/core'; + +import Camera from "../state/State/Scene/Camera"; +import { Vector3wUnits } from "../util/unit-math"; + + + +export const createArcRotateCamera = (camera: Camera.ArcRotate, bScene_: babylScene): ArcRotateCamera => { + const ret = new ArcRotateCamera('botcam', 0, 0, 0, Vector3wUnits.toBabylon(camera.target, 'centimeters'), bScene_); + ret.attachControl(bScene_.getEngine().getRenderingCanvas(), true); + ret.position = Vector3wUnits.toBabylon(camera.position, 'centimeters'); + ret.panningSensibility = 100; + // ret.checkCollisions = true; + return ret; +}; + +export const createNoneCamera = (camera: Camera.None, bScene_: babylScene): ArcRotateCamera => { + const ret = new ArcRotateCamera('botcam', 10, 10, 10, Vector3wUnits.toBabylon(Vector3wUnits.zero(), 'centimeters'), bScene_); + ret.attachControl(bScene_.getEngine().getRenderingCanvas(), true); + return ret; +}; + +export const createCamera = (camera: Camera, bScene_: babylScene): babylCamera => { + switch (camera.type) { + case 'arc-rotate': return createArcRotateCamera(camera, bScene_); + case 'none': return createNoneCamera(camera, bScene_); + } +}; \ No newline at end of file diff --git a/src/Bindings/Lightbinding.ts b/src/Bindings/Lightbinding.ts new file mode 100644 index 00000000..ab5b54e4 --- /dev/null +++ b/src/Bindings/Lightbinding.ts @@ -0,0 +1,58 @@ +import { ShadowGenerator, IShadowLight, PointLight, SpotLight, DirectionalLight, + Scene as babylScene } from '@babylonjs/core'; + +import Node from "../state/State/Scene/Node"; +import Dict from "../util/Dict"; +import { ReferenceFramewUnits, RotationwUnits, Vector3wUnits } from "../util/unit-math"; +import { RawQuaternion, RawVector2, RawVector3 } from "../util/math"; +import LocalizedString from '../util/LocalizedString'; +import { Angle, Distance, Mass, SetOps } from "../util"; + + +export const createDirectionalLight = (id: string, node: Node.DirectionalLight, bScene_: babylScene, shadowGenerators_: Dict<ShadowGenerator>): DirectionalLight => { + const ret = new DirectionalLight(node.name[LocalizedString.EN_US], RawVector3.toBabylon(node.direction), bScene_); + ret.intensity = node.intensity; + if (node.radius !== undefined) ret.radius = node.radius; + if (node.range !== undefined) ret.range = node.range; + shadowGenerators_[id] = createShadowGenerator_(ret); + return ret; +}; + +export const createSpotLight = (id: string, node: Node.SpotLight, bScene_: babylScene, shadowGenerators_: Dict<ShadowGenerator>): SpotLight => { + const origin: ReferenceFramewUnits = node.origin ?? {}; + const position: Vector3wUnits = origin.position ?? Vector3wUnits.zero(); + + const ret = new SpotLight( + node.name[LocalizedString.EN_US], + RawVector3.toBabylon(Vector3wUnits.toRaw(position, 'centimeters')), + RawVector3.toBabylon(node.direction), + Angle.toRadiansValue(node.angle), + node.exponent, + bScene_ + ); + shadowGenerators_[id] = createShadowGenerator_(ret); + return ret; +}; + +export const createPointLight = (id: string, node: Node.PointLight, bScene_: babylScene, shadowGenerators_: Dict<ShadowGenerator>): PointLight => { + const origin: ReferenceFramewUnits = node.origin ?? {}; + const position: Vector3wUnits = origin.position ?? Vector3wUnits.zero(); + const ret = new PointLight( + node.name[LocalizedString.EN_US], + RawVector3.toBabylon(Vector3wUnits.toRaw(position, 'centimeters')), + bScene_ + ); + ret.intensity = node.intensity; + + shadowGenerators_[id] = createShadowGenerator_(ret); + ret.setEnabled(node.visible); + return ret; +}; + +const createShadowGenerator_ = (light: IShadowLight) => { + const ret = new ShadowGenerator(1024, light); + ret.useKernelBlur = false; + ret.blurScale = 2; + ret.filter = ShadowGenerator.FILTER_POISSONSAMPLING; + return ret; +}; diff --git a/src/Bindings/MaterialBinding.ts b/src/Bindings/MaterialBinding.ts new file mode 100644 index 00000000..41e1a5db --- /dev/null +++ b/src/Bindings/MaterialBinding.ts @@ -0,0 +1,251 @@ +import { Texture, DynamicTexture, StandardMaterial, Color3, PBRMaterial, + Scene as babylScene, Material as babylMaterial, GlowLayer } from '@babylonjs/core'; + +import Material from '../state/State/Scene/Material'; +import { Color } from '../state/State/Scene/Color'; +import Patch from "../util/Patch"; + + + +export const createMaterial = (id: string, material: Material, bScene_: babylScene) => { + let bMaterial: babylMaterial; + switch (material.type) { + case 'basic': { + const basic = new StandardMaterial(id, bScene_); + const { color } = material; + if (color) { + switch (color.type) { + case 'color3': { + basic.diffuseColor = Color.toBabylon(color.color); + basic.diffuseTexture = null; + break; + } + case 'texture': { + if (!color.uri) { + basic.diffuseColor = new Color3(0.5, 0, 0.5); + } else { + if (id.includes('Sky')) { + basic.reflectionTexture = new Texture(color.uri, bScene_); + basic.reflectionTexture.coordinatesMode = Texture.FIXED_EQUIRECTANGULAR_MODE; + basic.backFaceCulling = false; + basic.disableLighting = true; + } else if (id === 'Container') { + const myDynamicTexture = new DynamicTexture("dynamic texture", 1000, bScene_, true); + // myDynamicTexture.drawText(material.text, 130, 600, "18px Arial", "white", "gray", true); + myDynamicTexture.drawText(color.uri, 130, 600, "18px Arial", "white", "gray", true); + basic.diffuseTexture = myDynamicTexture; + } else { + basic.bumpTexture = new Texture(color.uri, bScene_, false, false); + basic.emissiveTexture = new Texture(color.uri, bScene_, false, false); + basic.diffuseTexture = new Texture(color.uri, bScene_, false, false); + basic.diffuseTexture.coordinatesMode = Texture.FIXED_EQUIRECTANGULAR_MODE; + basic.backFaceCulling = false; + } + } + break; + } + } + } + bMaterial = basic; + break; + } + case 'pbr': { + const pbr = new PBRMaterial(id, bScene_); + const { albedo, ambient, emissive, metalness, reflection } = material; + if (albedo) { + switch (albedo.type) { + case 'color3': { + pbr.albedoColor = Color.toBabylon(albedo.color); + break; + } + case 'texture': { + pbr.albedoTexture = new Texture(albedo.uri, bScene_); + break; + } + } + } + if (ambient) { + switch (ambient.type) { + case 'color3': { + pbr.ambientColor = Color.toBabylon(ambient.color); + break; + } + case 'texture': { + pbr.ambientTexture = new Texture(ambient.uri, bScene_); + break; + } + } + } + if (emissive) { + const glow = new GlowLayer('glow', bScene_); + switch (emissive.type) { + case 'color3': { + pbr.emissiveColor = Color.toBabylon(emissive.color); + break; + } + case 'texture': { + pbr.emissiveTexture = new Texture(emissive.uri, bScene_); + break; + } + } + } + + if (metalness) { + switch (metalness.type) { + case 'color1': { + pbr.metallic = metalness.color; + break; + } + case 'texture': { + pbr.metallicTexture = new Texture(metalness.uri, bScene_); + break; + } + } + } + + if (reflection) { + switch (reflection.type) { + case 'color3': { + pbr.reflectivityColor = Color.toBabylon(reflection.color); + break; + } + case 'texture': { + pbr.reflectivityTexture = new Texture(reflection.uri, bScene_); + break; + } + } + } + bMaterial = pbr; + break; + } + } + return bMaterial; +}; + +export const updateMaterialBasic = (bMaterial: StandardMaterial, material: Patch.InnerPatch<Material.Basic>, bScene_: babylScene) => { + const { color } = material; + if (color.type === Patch.Type.InnerChange || color.type === Patch.Type.OuterChange) { + switch (color.next.type) { + case 'color3': { + bMaterial.diffuseColor = Color.toBabylon(color.next.color); + bMaterial.diffuseTexture = null; + break; + } + case 'texture': { + if (!color.next.uri) { + bMaterial.diffuseColor = new Color3(0.5, 0, 0.5); + bMaterial.diffuseTexture = null; + } else if (color.next.uri[0] !== '/') { + const myDynamicTexture = new DynamicTexture("dynamic texture", 1000, bScene_, true); + // myDynamicTexture.drawText(material.text, 130, 600, "18px Arial", "white", "gray", true); + myDynamicTexture.drawText(color.next.uri, 130, 600, "18px Arial", "white", "gray", true); + bMaterial.diffuseTexture = myDynamicTexture; + } else { + bMaterial.diffuseColor = Color.toBabylon(Color.WHITE); + bMaterial.diffuseTexture = new Texture(color.next.uri, bScene_); + } + break; + } + } + } + return bMaterial; +}; + +export const updateMaterialPbr = (bMaterial: PBRMaterial, material: Patch.InnerPatch<Material.Pbr>, bScene_: babylScene) => { + const { albedo, ambient, emissive, metalness, reflection } = material; + if (albedo.type === Patch.Type.OuterChange) { + switch (albedo.next.type) { + case 'color3': { + bMaterial.albedoColor = Color.toBabylon(albedo.next.color); + bMaterial.albedoTexture = null; + break; + } + case 'texture': { + if (!albedo.next.uri) { + bMaterial.albedoColor = new Color3(0.5, 0, 0.5); + } else { + bMaterial.albedoColor = Color.toBabylon(Color.WHITE); + bMaterial.albedoTexture = new Texture(albedo.next.uri, bScene_); + } + break; + } + } + } + if (ambient.type === Patch.Type.OuterChange) { + switch (ambient.next.type) { + case 'color3': { + bMaterial.ambientColor = Color.toBabylon(ambient.next.color); + bMaterial.ambientTexture = null; + break; + } + case 'texture': { + if (!ambient.next.uri) { + bMaterial.ambientColor = new Color3(0.5, 0, 0.5); + bMaterial.ambientTexture = null; + } else { + bMaterial.ambientColor = Color.toBabylon(Color.WHITE); + bMaterial.ambientTexture = new Texture(ambient.next.uri, bScene_); + } + break; + } + } + } + if (emissive.type === Patch.Type.OuterChange) { + switch (emissive.next.type) { + case 'color3': { + bMaterial.emissiveColor = Color.toBabylon(emissive.next.color); + bMaterial.emissiveTexture = null; + break; + } + case 'texture': { + if (!emissive.next.uri) { + bMaterial.emissiveColor = new Color3(0.5, 0, 0.5); + bMaterial.emissiveTexture = null; + } else { + bMaterial.emissiveColor = Color.toBabylon(Color.BLACK); + bMaterial.emissiveTexture = new Texture(emissive.next.uri, bScene_); + } + break; + } + } + } + if (metalness.type === Patch.Type.OuterChange) { + switch (metalness.next.type) { + case 'color1': { + bMaterial.metallic = metalness.next.color; + bMaterial.metallicTexture = null; + break; + } + case 'texture': { + if (!metalness.next.uri) { + bMaterial.metallic = 0; + } else { + bMaterial.metallicTexture = new Texture(metalness.next.uri, bScene_); + } + break; + } + } + } + if (reflection.type === Patch.Type.OuterChange) { + switch (reflection.next.type) { + case 'color3': { + bMaterial.reflectivityColor = Color.toBabylon(reflection.next.color); + bMaterial.reflectivityTexture = null; + break; + } + case 'texture': { + if (!reflection.next.uri) { + bMaterial.reflectivityColor = new Color3(0.5, 0, 0.5); + bMaterial.reflectivityTexture = null; + } else { + bMaterial.reflectivityColor = Color.toBabylon(Color.WHITE); + bMaterial.reflectivityTexture = new Texture(reflection.next.uri, bScene_); + } + break; + } + } + } + + return bMaterial; +}; + diff --git a/src/Bindings/MotorBindings.ts b/src/Bindings/MotorBindings.ts new file mode 100644 index 00000000..8ab78053 --- /dev/null +++ b/src/Bindings/MotorBindings.ts @@ -0,0 +1,80 @@ + +import { Scene as babylScene, Quaternion, Vector3, Mesh, + PhysicsConstraintAxis, Physics6DoFConstraint, + HingeConstraint, PhysicsConstraintAxisLimitMode +} from '@babylonjs/core'; + +import Node from '../state/State/Robot/Node'; +import { Vector3wUnits } from '../util/unit-math'; +import { RENDER_SCALE } from '../components/Constants/renderConstants'; +import { RawVector3 } from '../util/math'; + + + + +// Adds a physics constraint between a parent and child link. +export const createHinge_ = (id: string, hinge: Node.HingeJoint & { parentId: string }, bScene_: babylScene, bParent: Mesh, bChild: Mesh) => { + // Begin by moving the child in place (this prevents inertial snap as the physics engine applys the constraint) +// const { bParent, bChild } = this.bParentChild_(id, hinge.parentId); + bChild.setParent(bParent); + bChild.position.x = Vector3wUnits.toBabylon(hinge.parentPivot, 'meters')._x; + bChild.position.y = Vector3wUnits.toBabylon(hinge.parentPivot, 'meters')._y; + bChild.position.z = Vector3wUnits.toBabylon(hinge.parentPivot, 'meters')._z; + bChild.rotationQuaternion = Quaternion.FromEulerAngles(hinge.parentAxis.z * 3.1415 / 2, 0, 0); + + // The 6DoF constraint is used for motorized joints. Unforunately, it is not possible to + // completely lock these joints as hinges, so we also apply a hinge constraint. + // Order appears to matter here, the hinge should come before the 6DoF constraint. + if (id.includes("claw")) { + const hingeJoint = new HingeConstraint( + Vector3wUnits.toBabylon(hinge.parentPivot, RENDER_SCALE), + Vector3wUnits.toBabylon(hinge.childPivot, RENDER_SCALE), + new Vector3(0,0,1), + new Vector3(0,1,0), + bScene_ + ); + bParent.physicsBody.addConstraint(bChild.physicsBody, hingeJoint); + } else if (id.includes("arm")) { + const hingeJoint = new HingeConstraint( + Vector3wUnits.toBabylon(hinge.parentPivot, RENDER_SCALE), + Vector3wUnits.toBabylon(hinge.childPivot, RENDER_SCALE), + new Vector3(0,0,1), + new Vector3(0,-1,0), + bScene_ + ); + bParent.physicsBody.addConstraint(bChild.physicsBody, hingeJoint); + } else if (id.includes("wheel")) { + const hingeJoint = new HingeConstraint( + Vector3wUnits.toBabylon(hinge.parentPivot, RENDER_SCALE), + Vector3wUnits.toBabylon(hinge.childPivot, RENDER_SCALE), + new Vector3(0,1,0), + undefined, + bScene_ + ); + bParent.physicsBody.addConstraint(bChild.physicsBody, hingeJoint); + } + const joint: Physics6DoFConstraint = new Physics6DoFConstraint({ + pivotA: Vector3wUnits.toBabylon(hinge.parentPivot, RENDER_SCALE), + pivotB: Vector3wUnits.toBabylon(hinge.childPivot, RENDER_SCALE), + axisA: new Vector3(1,0,0), + axisB: new Vector3(1,0,0), + perpAxisA: new Vector3(0,-1,0), // bChildAxis, // + perpAxisB: RawVector3.toBabylon(hinge.parentAxis), + }, + [ + { + axis: PhysicsConstraintAxis.ANGULAR_Z, + minLimit: -30 * Math.PI / 180, maxLimit: -30 * Math.PI / 180, + } + ], + bScene_ + ); + + bParent.physicsBody.addConstraint(bChild.physicsBody, joint); + joint.setAxisMode(PhysicsConstraintAxis.LINEAR_X, PhysicsConstraintAxisLimitMode.LOCKED); + joint.setAxisMode(PhysicsConstraintAxis.LINEAR_Y, PhysicsConstraintAxisLimitMode.LOCKED); + joint.setAxisMode(PhysicsConstraintAxis.LINEAR_Z, PhysicsConstraintAxisLimitMode.LOCKED); + joint.setAxisMode(PhysicsConstraintAxis.ANGULAR_X, PhysicsConstraintAxisLimitMode.LOCKED); + joint.setAxisMode(PhysicsConstraintAxis.ANGULAR_Y, PhysicsConstraintAxisLimitMode.LOCKED); + return joint; +}; \ No newline at end of file diff --git a/src/Bindings/ObjectBinding.ts b/src/Bindings/ObjectBinding.ts new file mode 100644 index 00000000..4f3bda06 --- /dev/null +++ b/src/Bindings/ObjectBinding.ts @@ -0,0 +1,169 @@ +import { PhysicsShapeType, IPhysicsCollisionEvent, IPhysicsEnginePluginV2, PhysicsAggregate, + TransformNode, AbstractMesh, PhysicsViewer, ShadowGenerator, CreateBox, CreateSphere, CreateCylinder, + CreatePlane, Vector4, Vector3, Texture, DynamicTexture, StandardMaterial, GizmoManager, ArcRotateCamera, + IShadowLight, PointLight, SpotLight, DirectionalLight, Color3, PBRMaterial, Mesh, SceneLoader, EngineView, + Scene as babylScene, Node as babylNode, Camera as babylCamera, Material as babylMaterial, + GlowLayer, Observer, BoundingBox } from '@babylonjs/core'; + +import Geometry from "../state/State/Scene/Geometry"; +import { RawQuaternion, RawVector2, RawVector3 } from "../util/math"; +import { Angle, Distance, Mass, SetOps } from "../util"; +import Node from "../state/State/Scene/Node"; +import Scene from "../state/State/Scene"; +import LocalizedString from '../util/LocalizedString'; +import { apply } from './helpers'; +import { createMaterial } from './MaterialBinding'; +import { preBuiltGeometries, preBuiltTemplates } from "../NodeTemplates"; + +export type FrameLike = TransformNode | AbstractMesh; + +let seed_ = 1; + +const random = (max: number, min: number) => { + let x = Math.sin(seed_++) * 10000; + x = x - Math.floor(x); + x = ((x - .5) * (max - min)) + ((max + min) / 2); + return x; +}; + +export const buildGeometryFaceUvs = (faceUvs: RawVector2[] | undefined, expectedUvs: number): Vector4[] => { + if (faceUvs?.length !== expectedUvs) { + return undefined; + } + const ret: Vector4[] = []; + for (let i = 0; i + 1 < faceUvs.length; i += 2) { + ret.push(new Vector4(faceUvs[i].x, faceUvs[i].y, faceUvs[i + 1].x, faceUvs[i + 1].y)); + } + return ret; +}; + +export const buildGeometry = async (name: string, geometry: Geometry, bScene_: babylScene, faceUvs?: RawVector2[]): Promise<FrameLike> => { + let ret: FrameLike; + switch (geometry.type) { + case 'box': { + const rect = CreateBox(name, { + updatable:true, + width: Distance.toCentimetersValue(geometry.size.x), + height: Distance.toCentimetersValue(geometry.size.y), + depth: Distance.toCentimetersValue(geometry.size.z), + faceUV: buildGeometryFaceUvs(faceUvs, 12), + }, bScene_); + const verts = rect.getVerticesData("position"); + ret = rect; + break; + } + case 'sphere': { + const bFaceUvs = buildGeometryFaceUvs(faceUvs, 2)?.[0]; + const segments = 4; + const rock = CreateSphere(name, { + segments: segments, + updatable:true, + frontUVs: bFaceUvs, + sideOrientation: bFaceUvs ? Mesh.DOUBLESIDE : undefined, + diameterX:Distance.toCentimetersValue(geometry.radius) * 2, + diameterY:Distance.toCentimetersValue(geometry.radius) * 2 * geometry.squash, + diameterZ:Distance.toCentimetersValue(geometry.radius) * 2 * geometry.stretch, + }, bScene_); + + const positions = rock.getVerticesData("position"); + // TODO: Replace with custom rocks from blender + if (name.includes('Rock')) { + const skip = [25,26,38,39,51,52,64,65]; + for (let i = 14; i < 65; i++) { + if (skip.includes(i)) { + continue; + } else { + positions[3 * i] = positions[3 * i] + random(geometry.noise, -1 * geometry.noise); + positions[1 + 3 * i] = positions[1 + 3 * i] + random(geometry.noise, -1 * geometry.noise); + positions[2 + 3 * i] = positions[2 + 3 * i] + random(geometry.noise, -1 * geometry.noise); + } + } + } + rock.updateVerticesData("position", positions); + ret = rock; + break; + } + case 'cylinder': { + ret = CreateCylinder(name, { + height: Distance.toCentimetersValue(geometry.height), + diameterTop: Distance.toCentimetersValue(geometry.radius) * 2, + diameterBottom: Distance.toCentimetersValue(geometry.radius) * 2, + faceUV: buildGeometryFaceUvs(faceUvs, 6), + }, bScene_); + break; + } + case 'cone': { + ret = CreateCylinder(name, { + diameterTop: 0, + height: Distance.toCentimetersValue(geometry.height), + diameterBottom: Distance.toCentimetersValue(geometry.radius) * 2, + faceUV: buildGeometryFaceUvs(faceUvs, 6), + }, bScene_); + break; + } + case 'plane': { + ret = CreatePlane(name, { + width: Distance.toCentimetersValue(geometry.size.x), + height: Distance.toCentimetersValue(geometry.size.y), + frontUVs: buildGeometryFaceUvs(faceUvs, 2)?.[0], + }, bScene_); + break; + } + case 'file': { + const index = geometry.uri.lastIndexOf('/'); + const fileName = geometry.uri.substring(index + 1); + const baseName = geometry.uri.substring(0, index + 1); + + const res = await SceneLoader.ImportMeshAsync(geometry.include ?? '', baseName, fileName, bScene_); + if (res.meshes.length === 1) return res.meshes[0]; + // const nonColliders: Mesh[] = []; + ret = new TransformNode(geometry.uri, bScene_); + for (const mesh of res.meshes as Mesh[]) { + // GLTF importer adds a __root__ mesh (always the first one) that we can ignore + if (mesh.name === '__root__') continue; + // nonColliders.push(mesh); + mesh.setParent(ret); + } + // const mesh = Mesh.MergeMeshes(nonColliders, true, true, undefined, false, true); + break; + } + default: { + throw new Error(`Unsupported geometry type: ${geometry.type}`); + } + } + if (ret instanceof AbstractMesh) { + ret.visibility = 1; + } else { + const children = ret.getChildren(c => c instanceof AbstractMesh) as Mesh[]; + const mesh = Mesh.MergeMeshes(children, true, true, undefined, false, true); + mesh.visibility = 1; + ret = mesh; + } + return ret; +}; + +export const createObject = async (node: Node.Obj, nextScene: Scene, parent: babylNode, bScene_: babylScene): Promise<babylNode> => { + + const geometry = nextScene.geometry[node.geometryId] ?? preBuiltGeometries[node.geometryId]; + if (!geometry) { + console.error(`node ${LocalizedString.lookup(node.name, LocalizedString.EN_US)} has invalid geometry ID: ${node.geometryId}`); + return null; + } + const ret = await buildGeometry(node.name[LocalizedString.EN_US], geometry, bScene_, node.faceUvs); + if (!node.visible) { + apply(ret, m => m.isVisible = false); + } + if (node.material) { + const material = createMaterial(node.name[LocalizedString.EN_US], node.material, bScene_); + apply(ret, m => m.material = material); + } + ret.setParent(parent); + return ret; +}; + +export const createEmpty = (node: Node.Empty, parent: babylNode,bScene_: babylScene): TransformNode => { + + const ret = new TransformNode(node.name[LocalizedString.EN_US], bScene_); + ret.setParent(parent); + return ret; +}; diff --git a/src/RobotBinding.ts b/src/Bindings/RobotBinding.ts similarity index 73% rename from src/RobotBinding.ts rename to src/Bindings/RobotBinding.ts index b341d31b..9f85399f 100644 --- a/src/RobotBinding.ts +++ b/src/Bindings/RobotBinding.ts @@ -9,34 +9,21 @@ import { Scene as babylScene, TransformNode, AbstractMesh, LinesMesh, PhysicsVie import '@babylonjs/core/Physics/physicsEngineComponent'; -import SceneNode from './state/State/Scene/Node'; -import Robot from './state/State/Robot'; -import Node from './state/State/Robot/Node'; -import { RawQuaternion, RawVector3, clamp, RawEuler } from './util/math'; -import { ReferenceFramewUnits, RotationwUnits, Vector3wUnits } from './util/unit-math'; -import { Angle, Distance, Mass } from './util/Value'; +import SceneNode from '../state/State/Scene/Node'; +import Robot from '../state/State/Robot'; +import Node from '../state/State/Robot/Node'; +import { RawQuaternion, RawVector3, clamp, RawEuler } from '../util/math'; +import { ReferenceFramewUnits, RotationwUnits, Vector3wUnits } from '../util/unit-math'; +import { Angle, Distance, Mass } from '../util/Value'; import { SceneMeshMetadata } from './SceneBinding'; -import Geometry from './state/State/Robot/Geometry'; -import Dict from './util/Dict'; -import { RENDER_SCALE, RENDER_SCALE_METERS_MULTIPLIER } from './components/Constants/renderConstants'; -import WriteCommand from './AbstractRobot/WriteCommand'; -import AbstractRobot from './AbstractRobot'; -import Motor from './AbstractRobot/Motor'; -import { node } from 'prop-types'; - -interface BuiltGeometry { - nonColliders: Mesh[]; - colliders?: BuiltGeometry.Collider[]; -} - -namespace BuiltGeometry { - export interface Collider { - name: string; - mesh: Mesh; - type: number; - volume: number; - } -} +import Dict from '../util/Dict'; +import { RENDER_SCALE, RENDER_SCALE_METERS_MULTIPLIER } from '../components/Constants/renderConstants'; +import WriteCommand from '../AbstractRobot/WriteCommand'; +import AbstractRobot from '../AbstractRobot'; +import Motor from '../AbstractRobot/Motor'; +import { createLink_ } from './RobotLink'; +import { createHinge_ } from './MotorBindings'; +import { createWeight_ } from './WeightBinding'; class RobotBinding { private bScene_: babylScene; @@ -70,175 +57,30 @@ class RobotBinding { private physicsViewer_: PhysicsViewer; - constructor(bScene: babylScene, physicsViewer?: PhysicsViewer) { - this.bScene_ = bScene; - this.physicsViewer_ = physicsViewer; - } - // Loads the geometry of a robot part and divides into the collider and noncollider pieces - private buildGeometry_ = async (name: string, geometry: Geometry): Promise<BuiltGeometry> => { - let ret: BuiltGeometry; - - switch (geometry.type) { - case 'remote-mesh': { - const index = geometry.uri.lastIndexOf('/'); - const fileName = geometry.uri.substring(index + 1); - const baseName = geometry.uri.substring(0, index + 1); - - const res = await SceneLoader.ImportMeshAsync(geometry.include ?? '', baseName, fileName, this.bScene_); - - const nonColliders: Mesh[] = []; - const colliders: BuiltGeometry.Collider[] = []; - - for (const mesh of res.meshes.slice(1) as Mesh[]) { - - // The robot mesh includes sub-meshes with the 'collider' name to indicate their use. - if (mesh.name.startsWith('collider')) { - const parts = mesh.name.split('-'); - if (parts.length !== 3) throw new Error(`Invalid collider name: ${mesh.name}`); - const [_, type, name] = parts; - - const { extendSize } = mesh.getBoundingInfo().boundingBox; - - const volume = extendSize.x * extendSize.y * extendSize.z; - - let bType: number; - switch (type) { - case 'box': bType = PhysicsShapeType.BOX; break; - case 'sphere': bType = PhysicsShapeType.SPHERE; break; - case 'cylinder': bType = PhysicsShapeType.CYLINDER; break; - case 'capsule': bType = PhysicsShapeType.CAPSULE; break; - case 'plane': bType = PhysicsShapeType.HEIGHTFIELD; break; - case 'mesh': bType = PhysicsShapeType.MESH; break; - default: throw new Error(`Invalid collider type: ${type}`); - } - colliders.push({ mesh, type: bType, name, volume }); - } else { - nonColliders.push(mesh); - } - } + private lastTick_ = 0; + private lastMotorAngles_: [number, number, number, number] = [0, 0, 0, 0]; - ret = { nonColliders, colliders }; - break; - } - default: { throw new Error(`Unsupported geometry type: ${geometry.type}`); } - } - return ret; - }; + // Getting sensor values is async. We store the pending promises in these dictionaries. + private outstandingDigitalGetValues_: Dict<RobotBinding.OutstandingPromise<boolean>> = {}; + private outstandingAnalogGetValues_: Dict<RobotBinding.OutstandingPromise<number>> = {}; + private latestDigitalValues_: [boolean, boolean, boolean, boolean, boolean, boolean] = [false, false, false, false, false, false]; + private latestAnalogValues_: [number, number, number, number, number, number] = [0, 0, 0, 0, 0, 0]; - // Creates a link and returns the root mesh. Links are wheels, chasis, arms, etc. - // Loads the geometry and adds the appropriate physics properties to the mesh - private createLink_ = async (id: string, link: Node.Link) => { + private lastPErrs_: [number, number, number, number] = [0, 0, 0, 0]; + private iErrs_: [number, number, number, number] = [0, 0, 0, 0]; - let builtGeometry: BuiltGeometry; - if (link.geometryId === undefined) { - builtGeometry = { nonColliders: [new Mesh(id, this.bScene_)] }; - } else { - const geometry = this.robot_.geometry[link.geometryId]; - if (!geometry) throw new Error(`Missing geometry: ${link.geometryId}`); - builtGeometry = await this.buildGeometry_(id, geometry); - } - - const meshes = builtGeometry.nonColliders; - let myMesh: Mesh; + private brakeAt_: [number, number, number, number] = [undefined, undefined, undefined, undefined]; - const inertiaScale = .5; - - switch (link.collisionBody.type) { - // Notes on Links - the root link should have the highest mass and inertia and it should - // scale down further out the tree to prevent wild oscillations. - // body.setMassProperties can also help setting the inertia vector, - // body.setAngularDamping and body.setLinearDamping can help with oscillations - - case Node.Link.CollisionBody.Type.Box: { - // alert("box collision body"); // Currently there are no box collision bodies in the robot model - myMesh = Mesh.MergeMeshes(meshes, true, true, undefined, false, true); - myMesh.scaling.scaleInPlace(RENDER_SCALE_METERS_MULTIPLIER); - - const aggregate = new PhysicsAggregate(myMesh, PhysicsShapeType.BOX, { - mass: Mass.toGramsValue(link.mass || Mass.grams(10)), - friction: link.friction ?? 0.5, - restitution: link.restitution ?? 0, - startAsleep: true, - }, this.bScene_); - - this.colliders_.add(myMesh); - break; - } - case Node.Link.CollisionBody.Type.Cylinder: { - myMesh = Mesh.MergeMeshes(meshes, true, true, undefined, false, true); - myMesh.scaling.scaleInPlace(RENDER_SCALE_METERS_MULTIPLIER); - - const aggregate = new PhysicsAggregate(myMesh, PhysicsShapeType.CYLINDER, { - mass: Mass.toGramsValue(link.mass || Mass.grams(10)), - friction: link.friction ?? 0.5, - restitution: link.restitution ?? 0, - startAsleep: true, - }, this.bScene_); - - this.colliders_.add(myMesh); - break; - } - case Node.Link.CollisionBody.Type.Embedded: { - myMesh = Mesh.MergeMeshes(meshes, true, true, undefined, false, true); - myMesh.scaling.scaleInPlace(RENDER_SCALE_METERS_MULTIPLIER); - - // As the embedded collision body consists of multiple meshes, we need to create a parent - // This mmeans we are unable to use the physics aggregate - const parentShape = new PhysicsShapeContainer(this.bScene_); - - for (const collider of builtGeometry.colliders ?? []) { - const bCollider = collider.mesh; - bCollider.parent = myMesh; - - const parameters: PhysicsShapeParameters = { mesh: bCollider }; - const options: PhysicShapeOptions = { type: PhysicsShapeType.MESH, parameters: parameters }; - const shape = new PhysicsShape(options, this.bScene_); - shape.material = { - friction: link.friction ?? 0.5, - restitution: link.restitution ?? 0.1, - }; - - parentShape.addChild(shape, bCollider.absolutePosition, bCollider.absoluteRotationQuaternion); - - bCollider.visibility = 0; - this.colliders_.add(bCollider); - } - - const body = new PhysicsBody(myMesh, PhysicsMotionType.DYNAMIC, false, this.bScene_); - body.shape = parentShape; - if (link.geometryId.includes("chassis")) { - body.setMassProperties({ - mass: Mass.toGramsValue(link.mass || Mass.grams(100)), - inertia: new Vector3(10 * inertiaScale,10 * inertiaScale,10 * inertiaScale) // (left/right, twist around, rock forward and backward) - }); - } - if (link.geometryId.includes("arm")) { - body.setMassProperties({ - mass: Mass.toGramsValue(link.mass || Mass.grams(80)), - inertia: new Vector3(6 * inertiaScale,6 * inertiaScale,6 * inertiaScale) // (left/right, twist around, rock forward and backward) - }); - } - if (link.geometryId.includes("claw")) { - body.setMassProperties({ - mass: Mass.toGramsValue(link.mass || Mass.grams(10)), - inertia: new Vector3(3 * inertiaScale,3 * inertiaScale,3 * inertiaScale) // (left/right, twist around, rock forward and backward) - }); - } - body.setAngularDamping(.5); - - this.colliders_.add(myMesh); + private positionDeltaFracs_: [number, number, number, number] = [0, 0, 0, 0]; + private lastServoEnabledAngle_: [number, number, number, number] = [0, 0, 0, 0]; + + constructor(bScene: babylScene, physicsViewer?: PhysicsViewer) { + this.bScene_ = bScene; + this.physicsViewer_ = physicsViewer; + } - break; - } - default: { - throw new Error(`Unsupported collision body type: ${link.collisionBody.type}`); - } - } - if (this.physicsViewer_ && myMesh.physicsBody) this.physicsViewer_.showBody(myMesh.physicsBody); - return myMesh; - }; private createSensor_ = <T extends Node.FrameLike, O, S extends RobotBinding.SensorObject<T, O>>(s: { new(parameters: RobotBinding.SensorParameters<T>): S }) => (id: string, definition: T): S => { const parent = this.links_[definition.parentId]; @@ -253,67 +95,31 @@ class RobotBinding { }); }; + // These return functions are used to create sensors of a specific type. private createTouchSensor_ = this.createSensor_(RobotBinding.TouchSensor); private createEtSensor_ = this.createSensor_(RobotBinding.EtSensor); private createLightSensor_ = this.createSensor_(RobotBinding.LightSensor); private createReflectanceSensor_ = this.createSensor_(RobotBinding.ReflectanceSensor); - // Transform a vector using the child frame's orientation. This operation is invariant on a single - // axis, so we return a new quaternion with the leftover rotation. - private static connectedAxisAngle_ = (rotationAxis: Vector3, childOrientation: Quaternion): { axis: Vector3, twist: Quaternion } => { - const childOrientationInv = childOrientation.invert(); - const axis = rotationAxis.applyRotationQuaternion(childOrientationInv); - const v = new Vector3(childOrientationInv.x, childOrientationInv.y, childOrientationInv.z); - const s = childOrientationInv.w; - v.multiplyInPlace(axis); - - const twist = new Quaternion(v.x, v.y, v.z, s); - twist.normalize(); - - - return { - axis, - twist, - }; - }; - - // Adds an invisible weight to a parent link. - private createWeight_ = (id: string, weight: Node.Weight) => { - const ret = CreateSphere(id, { diameter: 1 }, this.bScene_); - ret.visibility = 0; - - const parent = this.robot_.nodes[weight.parentId]; - if (!parent) throw new Error(`Missing parent: "${weight.parentId}" for weight "${id}"`); - if (parent.type !== Node.Type.Link) throw new Error(`Invalid parent type: "${parent.type}" for weight "${id}"`); - - const aggregate = new PhysicsAggregate(ret, PhysicsShapeType.CYLINDER, { - mass: Mass.toGramsValue(weight.mass), - friction: 0, - restitution: 0, - }, this.bScene_); - - const bParent = this.links_[weight.parentId]; - if (!bParent) throw new Error(`Missing parent instantiation: "${weight.parentId}" for weight "${id}"`); - - const bOrigin = ReferenceFramewUnits.toBabylon(weight.origin, RENDER_SCALE); - - const constraint = new LockConstraint( - bOrigin.position, - new Vector3(0,0,0), // RawVector3.toBabylon(new Vector3(-8,10,0)), // updown, frontback - Vector3.Up(), - Vector3.Up().applyRotationQuaternion(bOrigin.rotationQuaternion.invert()), - this.bScene_ - ); - bParent.physicsBody.addConstraint(ret.physicsBody, constraint); + set realisticSensors(realisticSensors: boolean) { + for (const digitalSensor of Object.values(this.digitalSensors_)) { + digitalSensor.realistic = realisticSensors; + } + for (const analogSensor of Object.values(this.analogSensors_)) { + analogSensor.realistic = realisticSensors; + } + } - return ret; - }; + set noisySensors(noisySensors: boolean) { + for (const digitalSensor of Object.values(this.digitalSensors_)) { + digitalSensor.noisy = noisySensors; + } + for (const analogSensor of Object.values(this.analogSensors_)) { + analogSensor.noisy = noisySensors; + } + } - private bParentChild_ = (id: string, parentId: string): { - bParent: Mesh; - bChild: Mesh; - childId: string; - } => { + private bParentChild_ = (id: string, parentId: string): { bParent: Mesh; bChild: Mesh; childId: string; } => { if (!parentId) throw new Error(`Missing parent: "${parentId}" for node "${id}"`); const children = this.childrenNodeIds_[id]; @@ -334,107 +140,25 @@ class RobotBinding { }; }; - // Adds a physics constraint between a parent and child link. - private createHinge_ = (id: string, hinge: Node.HingeJoint & { parentId: string }) => { - - // Begin by moving the child in place (this prevents inertial snap as the physics engine applys the constraint) - const { bParent, bChild } = this.bParentChild_(id, hinge.parentId); - bChild.setParent(bParent); - bChild.position.x = Vector3wUnits.toBabylon(hinge.parentPivot, 'meters')._x; - bChild.position.y = Vector3wUnits.toBabylon(hinge.parentPivot, 'meters')._y; - bChild.position.z = Vector3wUnits.toBabylon(hinge.parentPivot, 'meters')._z; - - bChild.rotationQuaternion = Quaternion.FromEulerAngles(hinge.parentAxis.z * 3.1415 / 2, 0, 0); - - // The 6DoF constraint is used for motorized joints. Unforunately, it is not possible to - // completely lock these joints as hinges, so we also apply a hinge constraint. - // Order appears to matter here, the hinge should come before the 6DoF constraint. - if (id.includes("claw")) { - const hingeJoint = new HingeConstraint( - Vector3wUnits.toBabylon(hinge.parentPivot, RENDER_SCALE), - Vector3wUnits.toBabylon(hinge.childPivot, RENDER_SCALE), - new Vector3(0,0,1), - new Vector3(0,1,0), - this.bScene_ - ); - bParent.physicsBody.addConstraint(bChild.physicsBody, hingeJoint); - } else if (id.includes("arm")) { - const hingeJoint = new HingeConstraint( - Vector3wUnits.toBabylon(hinge.parentPivot, RENDER_SCALE), - Vector3wUnits.toBabylon(hinge.childPivot, RENDER_SCALE), - new Vector3(0,0,1), - new Vector3(0,-1,0), - this.bScene_ - ); - bParent.physicsBody.addConstraint(bChild.physicsBody, hingeJoint); - } else if (id.includes("wheel")) { - const hingeJoint = new HingeConstraint( - Vector3wUnits.toBabylon(hinge.parentPivot, RENDER_SCALE), - Vector3wUnits.toBabylon(hinge.childPivot, RENDER_SCALE), - new Vector3(0,1,0), - undefined, - this.bScene_ - ); - bParent.physicsBody.addConstraint(bChild.physicsBody, hingeJoint); - } - const joint: Physics6DoFConstraint = new Physics6DoFConstraint({ - pivotA: Vector3wUnits.toBabylon(hinge.parentPivot, RENDER_SCALE), - pivotB: Vector3wUnits.toBabylon(hinge.childPivot, RENDER_SCALE), - axisA: new Vector3(1,0,0), - axisB: new Vector3(1,0,0), - perpAxisA: new Vector3(0,-1,0), // bChildAxis, // - perpAxisB: RawVector3.toBabylon(hinge.parentAxis), - }, - [ - { - axis: PhysicsConstraintAxis.ANGULAR_Z, - minLimit: -30 * Math.PI / 180, maxLimit: -30 * Math.PI / 180, - } - ], - this.bScene_ - ); - - bParent.physicsBody.addConstraint(bChild.physicsBody, joint); - joint.setAxisMode(PhysicsConstraintAxis.LINEAR_X, PhysicsConstraintAxisLimitMode.LOCKED); - joint.setAxisMode(PhysicsConstraintAxis.LINEAR_Y, PhysicsConstraintAxisLimitMode.LOCKED); - joint.setAxisMode(PhysicsConstraintAxis.LINEAR_Z, PhysicsConstraintAxisLimitMode.LOCKED); - joint.setAxisMode(PhysicsConstraintAxis.ANGULAR_X, PhysicsConstraintAxisLimitMode.LOCKED); - joint.setAxisMode(PhysicsConstraintAxis.ANGULAR_Y, PhysicsConstraintAxisLimitMode.LOCKED); - - - return joint; - }; - - // Assumes the current hinge angle is the last target of the axis motor. - // TODO: implement a method using relative poses to get the actual angle. - private hingeAngle_ = (joint: Physics6DoFConstraint): number => { - const currentAngle: number = joint.getAxisMotorTarget(0); - return currentAngle; - }; - - - - private lastTick_ = 0; - private lastMotorAngles_: [number, number, number, number] = [0, 0, 0, 0]; - - // Getting sensor values is async. We store the pending promises in these dictionaries. - private outstandingDigitalGetValues_: Dict<RobotBinding.OutstandingPromise<boolean>> = {}; - private outstandingAnalogGetValues_: Dict<RobotBinding.OutstandingPromise<number>> = {}; - - private latestDigitalValues_: [boolean, boolean, boolean, boolean, boolean, boolean] = [false, false, false, false, false, false]; - private latestAnalogValues_: [number, number, number, number, number, number] = [0, 0, 0, 0, 0, 0]; - - private lastPErrs_: [number, number, number, number] = [0, 0, 0, 0]; - private iErrs_: [number, number, number, number] = [0, 0, 0, 0]; - - private brakeAt_: [number, number, number, number] = [undefined, undefined, undefined, undefined]; - - private positionDeltaFracs_: [number, number, number, number] = [0, 0, 0, 0]; - private lastServoEnabledAngle_: [number, number, number, number] = [0, 0, 0, 0]; - - + // Transform a vector using the child frame's orientation. This operation is invariant on a single + // axis, so we return a new quaternion with the leftover rotation. + // private static connectedAxisAngle_ = (rotationAxis: Vector3, childOrientation: Quaternion): { axis: Vector3, twist: Quaternion } => { + // const childOrientationInv = childOrientation.invert(); + // const axis = rotationAxis.applyRotationQuaternion(childOrientationInv); + // const v = new Vector3(childOrientationInv.x, childOrientationInv.y, childOrientationInv.z); + // const s = childOrientationInv.w; + // v.multiplyInPlace(axis); + // const twist = new Quaternion(v.x, v.y, v.z, s); + // twist.normalize(); + + // return { + // axis, + // twist, + // }; + // }; tick(readable: AbstractRobot.Readable): RobotBinding.TickOut { + // TODO: Motor Bindings need to be cleaned up to handle motor position explicitly. const motorPositionDeltas: [number, number, number, number] = [0, 0, 0, 0]; const writeCommands: WriteCommand[] = []; @@ -467,7 +191,10 @@ class RobotBinding { const plug = (motorNode.plug === undefined || motorNode.plug === 'normal') ? 1 : -1; // Get the delta angle of the motor since the last tick. - const currentAngle = this.hingeAngle_(bMotor); + // Assumes the current hinge angle is the last target of the axis motor. + // TODO: implement a method using relative poses to get the actual angle. + const currentAngle: number = bMotor.getAxisMotorTarget(0); + // const currentAngle = this.hingeAngle_(bMotor); const lastMotorAngle = this.lastMotorAngles_[port]; let deltaAngle = 0; if (lastMotorAngle > Math.PI / 2 && currentAngle < -Math.PI / 2) { @@ -674,6 +401,7 @@ class RobotBinding { writeCommands.push(WriteCommand.digitalIn({ port: i, value: digitalValues[i] })); } + // Analog Sensors const analogValues: AbstractRobot.Stateless.AnalogValues = [0, 0, 0, 0, 0, 0]; for (let i = 0; i < 6; ++i) { const analogId = this.analogPorts_[i]; @@ -831,46 +559,46 @@ class RobotBinding { this.robot_ = robot; + // Set Root const rootIds = Robot.rootNodeIds(robot); if (rootIds.length !== 1) throw new Error('Only one root node is supported'); - this.rootId_ = rootIds[0]; - - const nodeIds = Robot.breadthFirstNodeIds(robot); - this.childrenNodeIds_ = Robot.childrenNodeIds(robot); - const rootNode = robot.nodes[this.rootId_]; - if (robot.nodes[this.rootId_].type !== Node.Type.Link) throw new Error('Root node must be a link'); + const nodeIds = Robot.breadthFirstNodeIds(robot); + this.childrenNodeIds_ = Robot.childrenNodeIds(robot); + // Links need to be set up before the rest of the nodes for (const nodeId of nodeIds) { const node = robot.nodes[nodeId]; if (node.type !== Node.Type.Link) continue; - const bNode = await this.createLink_(nodeId, node); + const bNode = await createLink_(nodeId, node, this.bScene_, this.robot_, this.colliders_); + if (this.physicsViewer_ && bNode.physicsBody) this.physicsViewer_.showBody(bNode.physicsBody); + bNode.metadata = { id: this.robotSceneId_, selected: false } as SceneMeshMetadata; this.links_[nodeId] = bNode; } + // Set up all other types of nodes for (const nodeId of nodeIds) { const node = robot.nodes[nodeId]; if (node.type === Node.Type.Link) continue; switch (node.type) { case Node.Type.Weight: { - const bNode = this.createWeight_(nodeId, node); + const bNode = createWeight_(nodeId, node, this.bScene_, this.robot_, this.links_); this.weights_[nodeId] = bNode; break; } case Node.Type.Motor: { - const bJoint = this.createHinge_(nodeId, node); + const { bParent, bChild } = this.bParentChild_(nodeId, node.parentId); + const bJoint = createHinge_(nodeId, node, this.bScene_, bParent, bChild); + bJoint.setAxisMotorMaxForce(PhysicsConstraintAxis.ANGULAR_Z, 1000000000); - bJoint.setAxisMaxLimit(PhysicsConstraintAxis.ANGULAR_Z, 1000000000000); - bJoint.setAxisMinLimit(PhysicsConstraintAxis.ANGULAR_Z, -1000000000000); - bJoint.setAxisMotorTarget(PhysicsConstraintAxis.ANGULAR_Z, 0); - bJoint.setAxisMotorType(PhysicsConstraintAxis.ANGULAR_Z, PhysicsConstraintMotorType.VELOCITY); // Position control - + bJoint.setAxisMotorType(PhysicsConstraintAxis.ANGULAR_Z, PhysicsConstraintMotorType.VELOCITY); + // Start motor in locked position so the wheels don't slide bJoint.setAxisMode(PhysicsConstraintAxis.ANGULAR_Z, PhysicsConstraintAxisLimitMode.LOCKED); this.motors_[nodeId] = bJoint; @@ -880,17 +608,17 @@ class RobotBinding { case Node.Type.Servo: { // minLimit: -30 * Math.PI / 180, maxLimit: -30 * Math.PI / 180, // -90 is upright and closed; 0 is forward and open - const bJoint = this.createHinge_(nodeId, node); + const { bParent, bChild } = this.bParentChild_(nodeId, node.parentId); + const bJoint = createHinge_(nodeId, node, this.bScene_, bParent, bChild); bJoint.setAxisMotorMaxForce(PhysicsConstraintAxis.ANGULAR_Z, 10000000); - bJoint.setAxisMotorTarget(PhysicsConstraintAxis.ANGULAR_Z, 2); - bJoint.setAxisMotorType(PhysicsConstraintAxis.ANGULAR_Z, PhysicsConstraintMotorType.VELOCITY); // Velocity control - + bJoint.setAxisMotorType(PhysicsConstraintAxis.ANGULAR_Z, PhysicsConstraintMotorType.VELOCITY); + // Start the servos at 0 bJoint.setAxisMaxLimit(PhysicsConstraintAxis.ANGULAR_Z, Angle.toRadiansValue(Angle.degrees(0))); - bJoint.setAxisMinLimit(PhysicsConstraintAxis.ANGULAR_Z, Angle.toRadiansValue(Angle.degrees(-10))); + bJoint.setAxisMinLimit(PhysicsConstraintAxis.ANGULAR_Z, Angle.toRadiansValue(Angle.degrees(-1))); this.servos_[nodeId] = bJoint; this.servoPorts_[node.servoPort] = nodeId; - this.lastServoEnabledAngle_[node.servoPort] = Angle.toRadiansValue(Angle.degrees(-10)); + this.lastServoEnabledAngle_[node.servoPort] = Angle.toRadiansValue(Angle.degrees(-1)); break; } case Node.Type.TouchSensor: { @@ -939,25 +667,10 @@ class RobotBinding { this.robot_ = null; } +} + - set realisticSensors(realisticSensors: boolean) { - for (const digitalSensor of Object.values(this.digitalSensors_)) { - digitalSensor.realistic = realisticSensors; - } - for (const analogSensor of Object.values(this.analogSensors_)) { - analogSensor.realistic = realisticSensors; - } - } - set noisySensors(noisySensors: boolean) { - for (const digitalSensor of Object.values(this.digitalSensors_)) { - digitalSensor.noisy = noisySensors; - } - for (const analogSensor of Object.values(this.analogSensors_)) { - analogSensor.noisy = noisySensors; - } - } -} namespace RobotBinding { export interface TickOut { diff --git a/src/Bindings/RobotLink.ts b/src/Bindings/RobotLink.ts new file mode 100644 index 00000000..e07b9f1b --- /dev/null +++ b/src/Bindings/RobotLink.ts @@ -0,0 +1,160 @@ + +import { Scene as babylScene, Vector3, Mesh, SceneLoader, + PhysicsBody, PhysicsMotionType, PhysicsShape, + PhysicsAggregate, PhysicsShapeType, PhysicShapeOptions, + PhysicsShapeParameters, PhysicsShapeContainer +} from '@babylonjs/core'; + +import Geometry from '../state/State/Robot/Geometry'; +import Node from '../state/State/Robot/Node'; +import { RENDER_SCALE_METERS_MULTIPLIER } from '../components/Constants/renderConstants'; +import Robot from '../state/State/Robot'; +import { Mass } from '../util/Value'; + + +interface BuiltGeometry { + nonColliders: Mesh[]; + colliders?: BuiltGeometry.Collider[]; +} + +namespace BuiltGeometry { + export interface Collider { + name: string; + mesh: Mesh; + type: number; + volume: number; + } +} + +// Loads the geometry of a robot part and divides into the collider and noncollider pieces +export const buildGeometry_ = async (name: string, geometry: Geometry, bScene_: babylScene): Promise<BuiltGeometry> => { + let ret: BuiltGeometry; + switch (geometry.type) { + case 'remote-mesh': { + const index = geometry.uri.lastIndexOf('/'); + const fileName = geometry.uri.substring(index + 1); + const baseName = geometry.uri.substring(0, index + 1); + + const res = await SceneLoader.ImportMeshAsync(geometry.include ?? '', baseName, fileName, bScene_); + + const nonColliders: Mesh[] = []; + const colliders: BuiltGeometry.Collider[] = []; + for (const mesh of res.meshes.slice(1) as Mesh[]) { + // The robot mesh includes sub-meshes with the 'collider' name to indicate their use. + if (mesh.name.startsWith('collider')) { + const parts = mesh.name.split('-'); + if (parts.length !== 3) throw new Error(`Invalid collider name: ${mesh.name}`); + const [_, type, name] = parts; + const { extendSize } = mesh.getBoundingInfo().boundingBox; + const volume = extendSize.x * extendSize.y * extendSize.z; + let bType: number; + switch (type) { + case 'box': bType = PhysicsShapeType.BOX; break; + case 'sphere': bType = PhysicsShapeType.SPHERE; break; + case 'cylinder': bType = PhysicsShapeType.CYLINDER; break; + case 'capsule': bType = PhysicsShapeType.CAPSULE; break; + case 'plane': bType = PhysicsShapeType.HEIGHTFIELD; break; + case 'mesh': bType = PhysicsShapeType.MESH; break; + default: throw new Error(`Invalid collider type: ${type}`); + } + colliders.push({ mesh, type: bType, name, volume }); + } else { + nonColliders.push(mesh); + } + } + ret = { nonColliders, colliders }; + break; + } + default: { throw new Error(`Unsupported geometry type: ${geometry.type}`); } + } + return ret; +}; + + + +// Creates a link and returns the root mesh. Links are wheels, chasis, arms, etc. +// Loads the geometry and adds the appropriate physics properties to the mesh +export const createLink_ = async (id: string, link: Node.Link, bScene_: babylScene, robot_: Robot, colliders_: Set<Mesh>) => { + let builtGeometry: BuiltGeometry; + if (link.geometryId === undefined) { + builtGeometry = { nonColliders: [new Mesh(id, bScene_)] }; + } else { + const geometry = robot_.geometry[link.geometryId]; + if (!geometry) throw new Error(`Missing geometry: ${link.geometryId}`); + builtGeometry = await buildGeometry_(id, geometry, bScene_); + } + + const meshes = builtGeometry.nonColliders; + let myMesh: Mesh; + const inertiaScale = 1; + + switch (link.collisionBody.type) { + // Notes on Links - the root link should have the highest mass and inertia and it should + // scale down further out the tree to prevent wild oscillations. + // body.setMassProperties can also help setting the inertia vector, + // body.setAngularDamping and body.setLinearDamping can help with oscillations + case Node.Link.CollisionBody.Type.Box: { + // alert("box collision body"); // Currently there are no box collision bodies in the robot model + myMesh = Mesh.MergeMeshes(meshes, true, true, undefined, false, true); + myMesh.scaling.scaleInPlace(RENDER_SCALE_METERS_MULTIPLIER); + const aggregate = new PhysicsAggregate(myMesh, PhysicsShapeType.BOX, { + mass: Mass.toGramsValue(link.mass || Mass.grams(10)), + friction: link.friction ?? 0.5, + restitution: link.restitution ?? 0, + startAsleep: true, + }, bScene_); + colliders_.add(myMesh); + break; + } + case Node.Link.CollisionBody.Type.Cylinder: { + myMesh = Mesh.MergeMeshes(meshes, true, true, undefined, false, true); + myMesh.scaling.scaleInPlace(RENDER_SCALE_METERS_MULTIPLIER); + const aggregate = new PhysicsAggregate(myMesh, PhysicsShapeType.CYLINDER, { + mass: Mass.toGramsValue(link.mass || Mass.grams(10)), + friction: link.friction ?? 0.5, + restitution: link.restitution ?? 0, + startAsleep: true, + }, bScene_); + colliders_.add(myMesh); + break; + } + case Node.Link.CollisionBody.Type.Embedded: { + myMesh = Mesh.MergeMeshes(meshes, true, true, undefined, false, true); + myMesh.scaling.scaleInPlace(RENDER_SCALE_METERS_MULTIPLIER); + // As the embedded collision body consists of multiple meshes, we need to create a parent + // This mmeans we are unable to use the physics aggregate + const parentShape = new PhysicsShapeContainer(bScene_); + for (const collider of builtGeometry.colliders ?? []) { + const bCollider = collider.mesh; + bCollider.parent = myMesh; + const parameters: PhysicsShapeParameters = { mesh: bCollider }; + const options: PhysicShapeOptions = { type: PhysicsShapeType.MESH, parameters: parameters }; + const shape = new PhysicsShape(options, bScene_); + shape.material = { + friction: link.friction ?? 0.5, + restitution: link.restitution ?? 0.1, + }; + parentShape.addChild(shape, bCollider.absolutePosition, bCollider.absoluteRotationQuaternion); + bCollider.visibility = 0; + colliders_.add(bCollider); + } + + const body = new PhysicsBody(myMesh, PhysicsMotionType.DYNAMIC, false, bScene_); + body.shape = parentShape; + if (link.inertia) { + body.setMassProperties({ + mass: Mass.toGramsValue(link.mass), + inertia: new Vector3(link.inertia[0], link.inertia[1], link.inertia[2]) // (left/right, twist around, rock forward and backward) + }); + } + body.setAngularDamping(.5); + + colliders_.add(myMesh); + break; + } + default: { + throw new Error(`Unsupported collision body type: ${link.collisionBody.type}`); + } + } + return myMesh; +}; \ No newline at end of file diff --git a/src/SceneBinding.ts b/src/Bindings/SceneBinding.ts similarity index 63% rename from src/SceneBinding.ts rename to src/Bindings/SceneBinding.ts index edfd587c..4cc03317 100644 --- a/src/SceneBinding.ts +++ b/src/Bindings/SceneBinding.ts @@ -10,28 +10,34 @@ import { PhysicsShapeType, IPhysicsCollisionEvent, IPhysicsEnginePluginV2, Physi import '@babylonjs/core/Engines/Extensions/engine.views'; import '@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent'; -import Dict from "./util/Dict"; -import { RawQuaternion, RawVector2, RawVector3 } from "./util/math"; -import Scene from "./state/State/Scene"; -import Camera from "./state/State/Scene/Camera"; -import Geometry from "./state/State/Scene/Geometry"; -import Node from "./state/State/Scene/Node"; -import Patch from "./util/Patch"; - -import { ReferenceFramewUnits, RotationwUnits, Vector3wUnits } from "./util/unit-math"; -import { Angle, Distance, Mass, SetOps } from "./util"; -import { Color } from './state/State/Scene/Color'; -import Material from './state/State/Scene/Material'; -import { preBuiltGeometries, preBuiltTemplates } from "./NodeTemplates"; +import Dict from "../util/Dict"; +import { RawQuaternion, RawVector2, RawVector3 } from "../util/math"; +import Scene from "../state/State/Scene"; +import Camera from "../state/State/Scene/Camera"; +import Geometry from "../state/State/Scene/Geometry"; +import Node from "../state/State/Scene/Node"; +import Patch from "../util/Patch"; + +import { ReferenceFramewUnits, RotationwUnits, Vector3wUnits } from "../util/unit-math"; +import { Angle, Distance, Mass, SetOps } from "../util"; +import { Color } from '../state/State/Scene/Color'; +import Material from '../state/State/Scene/Material'; +import { preBuiltGeometries, preBuiltTemplates } from "../NodeTemplates"; import RobotBinding from './RobotBinding'; -import Robot from './state/State/Robot'; -import AbstractRobot from './AbstractRobot'; -import WorkerInstance from "./programming/WorkerInstance"; -import LocalizedString from './util/LocalizedString'; -import ScriptManager from './ScriptManager'; -import { RENDER_SCALE } from './components/Constants/renderConstants'; +import Robot from '../state/State/Robot'; +import AbstractRobot from '../AbstractRobot'; +import WorkerInstance from "../programming/WorkerInstance"; +import LocalizedString from '../util/LocalizedString'; +import ScriptManager from '../ScriptManager'; +import { RENDER_SCALE } from '../components/Constants/renderConstants'; import { number } from 'prop-types'; +import { createMaterial, updateMaterialBasic, updateMaterialPbr } from './MaterialBinding'; +import { createDirectionalLight, createPointLight, createSpotLight } from './Lightbinding'; +import { createCamera } from './CameraBinding'; +import { createObject, createEmpty } from './ObjectBinding'; +import { apply } from './helpers'; + export type FrameLike = TransformNode | AbstractMesh; export interface SceneMeshMetadata { @@ -92,8 +98,6 @@ class SceneBinding { private materialIdIter_ = 0; - private seed_ = 0; - constructor(bScene: babylScene, physics: IPhysicsEnginePluginV2) { this.bScene_ = bScene; this.scene_ = Scene.EMPTY; @@ -111,7 +115,7 @@ class SceneBinding { this.root_ = new TransformNode('__scene_root__', this.bScene_); this.gizmoManager_ = new GizmoManager(this.bScene_); - this.camera_ = this.createNoneCamera_(Camera.NONE); + this.camera_ = createCamera(Camera.NONE, this.bScene_); // Gizmos are the little arrows that appear when you select an object this.gizmoManager_.positionGizmoEnabled = true; @@ -140,408 +144,13 @@ class SceneBinding { return ret; } - // apply_ is used to propogate the function f(m) on children of the specified mesh g - private static apply_ = (g: babylNode, f: (m: AbstractMesh) => void) => { - if (g instanceof AbstractMesh) { - f(g); - } else { - (g.getChildren(c => c instanceof AbstractMesh) as AbstractMesh[]).forEach(f); - } - }; - - private random = (max: number, min: number) => { - let x = Math.sin(this.seed_++) * 10000; - x = x - Math.floor(x); - x = ((x - .5) * (max - min)) + ((max + min) / 2); - return x; - }; - - private buildGeometry_ = async (name: string, geometry: Geometry, faceUvs?: RawVector2[]): Promise<FrameLike> => { - let ret: FrameLike; - switch (geometry.type) { - case 'box': { - const rect = CreateBox(name, { - updatable:true, - width: Distance.toCentimetersValue(geometry.size.x), - height: Distance.toCentimetersValue(geometry.size.y), - depth: Distance.toCentimetersValue(geometry.size.z), - faceUV: this.buildGeometryFaceUvs_(faceUvs, 12), - }, this.bScene_); - const verts = rect.getVerticesData("position"); - ret = rect; - break; - } - case 'sphere': { - const bFaceUvs = this.buildGeometryFaceUvs_(faceUvs, 2)?.[0]; - const segments = 4; - const rock = CreateSphere(name, { - segments: segments, - updatable:true, - frontUVs: bFaceUvs, - sideOrientation: bFaceUvs ? Mesh.DOUBLESIDE : undefined, - diameterX:Distance.toCentimetersValue(geometry.radius) * 2, - diameterY:Distance.toCentimetersValue(geometry.radius) * 2 * geometry.squash, - diameterZ:Distance.toCentimetersValue(geometry.radius) * 2 * geometry.stretch, - }, this.bScene_); - - const positions = rock.getVerticesData("position"); - // TODO: Replace with custom rocks from blender - if (name.includes('Rock')) { - const skip = [25,26,38,39,51,52,64,65]; - for (let i = 14; i < 65; i++) { - if (skip.includes(i)) { - continue; - } else { - positions[3 * i] = positions[3 * i] + this.random(geometry.noise, -1 * geometry.noise); - positions[1 + 3 * i] = positions[1 + 3 * i] + this.random(geometry.noise, -1 * geometry.noise); - positions[2 + 3 * i] = positions[2 + 3 * i] + this.random(geometry.noise, -1 * geometry.noise); - } - } - } - rock.updateVerticesData("position", positions); - - ret = rock; - break; - } - case 'cylinder': { - ret = CreateCylinder(name, { - height: Distance.toCentimetersValue(geometry.height), - diameterTop: Distance.toCentimetersValue(geometry.radius) * 2, - diameterBottom: Distance.toCentimetersValue(geometry.radius) * 2, - faceUV: this.buildGeometryFaceUvs_(faceUvs, 6), - }, this.bScene_); - break; - } - case 'cone': { - ret = CreateCylinder(name, { - diameterTop: 0, - height: Distance.toCentimetersValue(geometry.height), - diameterBottom: Distance.toCentimetersValue(geometry.radius) * 2, - faceUV: this.buildGeometryFaceUvs_(faceUvs, 6), - }, this.bScene_); - break; - } - case 'plane': { - ret = CreatePlane(name, { - width: Distance.toCentimetersValue(geometry.size.x), - height: Distance.toCentimetersValue(geometry.size.y), - frontUVs: this.buildGeometryFaceUvs_(faceUvs, 2)?.[0], - }, this.bScene_); - break; - } - case 'file': { - const index = geometry.uri.lastIndexOf('/'); - const fileName = geometry.uri.substring(index + 1); - const baseName = geometry.uri.substring(0, index + 1); - - const res = await SceneLoader.ImportMeshAsync(geometry.include ?? '', baseName, fileName, this.bScene_); - if (res.meshes.length === 1) return res.meshes[0]; - // const nonColliders: Mesh[] = []; - - ret = new TransformNode(geometry.uri, this.bScene_); - for (const mesh of res.meshes as Mesh[]) { - // GLTF importer adds a __root__ mesh (always the first one) that we can ignore - if (mesh.name === '__root__') continue; - // nonColliders.push(mesh); - - mesh.setParent(ret); - } - // const mesh = Mesh.MergeMeshes(nonColliders, true, true, undefined, false, true); - break; - } - default: { - throw new Error(`Unsupported geometry type: ${geometry.type}`); - } - } - - if (ret instanceof AbstractMesh) { - ret.visibility = 1; - } else { - const children = ret.getChildren(c => c instanceof AbstractMesh) as Mesh[]; - const mesh = Mesh.MergeMeshes(children, true, true, undefined, false, true); - mesh.visibility = 1; - ret = mesh; - } - - return ret; - }; - - private buildGeometryFaceUvs_ = (faceUvs: RawVector2[] | undefined, expectedUvs: number): Vector4[] => { - if (faceUvs?.length !== expectedUvs) { - return undefined; - } - - const ret: Vector4[] = []; - for (let i = 0; i + 1 < faceUvs.length; i += 2) { - ret.push(new Vector4(faceUvs[i].x, faceUvs[i].y, faceUvs[i + 1].x, faceUvs[i + 1].y)); - } - - return ret; - }; - private findBNode_ = (id?: string, defaultToRoot?: boolean): babylNode => { if (id === undefined && defaultToRoot) return this.root_; if (id !== undefined && !(id in this.nodes_)) throw new Error(`${id} doesn't exist`); return this.nodes_[id]; }; - private createMaterial_ = (id: string, material: Material) => { - - let bMaterial: babylMaterial; - switch (material.type) { - case 'basic': { - const basic = new StandardMaterial(id, this.bScene_); - const { color } = material; - - if (color) { - switch (color.type) { - case 'color3': { - basic.diffuseColor = Color.toBabylon(color.color); - basic.diffuseTexture = null; - break; - } - case 'texture': { - if (!color.uri) { - basic.diffuseColor = new Color3(0.5, 0, 0.5); - } else { - if (id.includes('Sky')) { - basic.reflectionTexture = new Texture(color.uri, this.bScene_); - basic.reflectionTexture.coordinatesMode = Texture.FIXED_EQUIRECTANGULAR_MODE; - basic.backFaceCulling = false; - basic.disableLighting = true; - } else if (id === 'Container') { - const myDynamicTexture = new DynamicTexture("dynamic texture", 1000, this.bScene_, true); - // myDynamicTexture.drawText(material.text, 130, 600, "18px Arial", "white", "gray", true); - myDynamicTexture.drawText(color.uri, 130, 600, "18px Arial", "white", "gray", true); - basic.diffuseTexture = myDynamicTexture; - } else { - basic.bumpTexture = new Texture(color.uri, this.bScene_, false, false); - basic.emissiveTexture = new Texture(color.uri, this.bScene_, false, false); - basic.diffuseTexture = new Texture(color.uri, this.bScene_, false, false); - basic.diffuseTexture.coordinatesMode = Texture.FIXED_EQUIRECTANGULAR_MODE; - basic.backFaceCulling = false; - } - } - break; - } - } - } - bMaterial = basic; - break; - } - case 'pbr': { - const pbr = new PBRMaterial(id, this.bScene_); - const { albedo, ambient, emissive, metalness, reflection } = material; - if (albedo) { - switch (albedo.type) { - case 'color3': { - pbr.albedoColor = Color.toBabylon(albedo.color); - break; - } - case 'texture': { - pbr.albedoTexture = new Texture(albedo.uri, this.bScene_); - break; - } - } - } - - if (ambient) { - switch (ambient.type) { - case 'color3': { - pbr.ambientColor = Color.toBabylon(ambient.color); - break; - } - case 'texture': { - pbr.ambientTexture = new Texture(ambient.uri, this.bScene_); - break; - } - } - } - - if (emissive) { - const glow = new GlowLayer('glow', this.bScene_); - switch (emissive.type) { - case 'color3': { - pbr.emissiveColor = Color.toBabylon(emissive.color); - break; - } - case 'texture': { - pbr.emissiveTexture = new Texture(emissive.uri, this.bScene_); - break; - } - } - } - - if (metalness) { - switch (metalness.type) { - case 'color1': { - pbr.metallic = metalness.color; - break; - } - case 'texture': { - pbr.metallicTexture = new Texture(metalness.uri, this.bScene_); - break; - } - } - } - - if (reflection) { - switch (reflection.type) { - case 'color3': { - pbr.reflectivityColor = Color.toBabylon(reflection.color); - break; - } - case 'texture': { - pbr.reflectivityTexture = new Texture(reflection.uri, this.bScene_); - break; - } - } - } - - bMaterial = pbr; - - break; - } - } - - return bMaterial; - }; - - private updateMaterialBasic_ = (bMaterial: StandardMaterial, material: Patch.InnerPatch<Material.Basic>) => { - const { color } = material; - - if (color.type === Patch.Type.InnerChange || color.type === Patch.Type.OuterChange) { - switch (color.next.type) { - case 'color3': { - - bMaterial.diffuseColor = Color.toBabylon(color.next.color); - bMaterial.diffuseTexture = null; - break; - } - case 'texture': { - if (!color.next.uri) { - bMaterial.diffuseColor = new Color3(0.5, 0, 0.5); - bMaterial.diffuseTexture = null; - } else if (color.next.uri[0] !== '/') { - const myDynamicTexture = new DynamicTexture("dynamic texture", 1000, this.bScene_, true); - // myDynamicTexture.drawText(material.text, 130, 600, "18px Arial", "white", "gray", true); - myDynamicTexture.drawText(color.next.uri, 130, 600, "18px Arial", "white", "gray", true); - bMaterial.diffuseTexture = myDynamicTexture; - } else { - bMaterial.diffuseColor = Color.toBabylon(Color.WHITE); - bMaterial.diffuseTexture = new Texture(color.next.uri, this.bScene_); - } - break; - } - } - } - - return bMaterial; - }; - - private updateMaterialPbr_ = (bMaterial: PBRMaterial, material: Patch.InnerPatch<Material.Pbr>) => { - const { albedo, ambient, emissive, metalness, reflection } = material; - - if (albedo.type === Patch.Type.OuterChange) { - switch (albedo.next.type) { - case 'color3': { - bMaterial.albedoColor = Color.toBabylon(albedo.next.color); - bMaterial.albedoTexture = null; - break; - } - case 'texture': { - if (!albedo.next.uri) { - bMaterial.albedoColor = new Color3(0.5, 0, 0.5); - } else { - bMaterial.albedoColor = Color.toBabylon(Color.WHITE); - bMaterial.albedoTexture = new Texture(albedo.next.uri, this.bScene_); - } - break; - } - } - } - - if (ambient.type === Patch.Type.OuterChange) { - switch (ambient.next.type) { - case 'color3': { - bMaterial.ambientColor = Color.toBabylon(ambient.next.color); - bMaterial.ambientTexture = null; - break; - } - case 'texture': { - if (!ambient.next.uri) { - bMaterial.ambientColor = new Color3(0.5, 0, 0.5); - bMaterial.ambientTexture = null; - } else { - bMaterial.ambientColor = Color.toBabylon(Color.WHITE); - bMaterial.ambientTexture = new Texture(ambient.next.uri, this.bScene_); - } - break; - } - } - } - - if (emissive.type === Patch.Type.OuterChange) { - switch (emissive.next.type) { - case 'color3': { - bMaterial.emissiveColor = Color.toBabylon(emissive.next.color); - bMaterial.emissiveTexture = null; - break; - } - case 'texture': { - if (!emissive.next.uri) { - bMaterial.emissiveColor = new Color3(0.5, 0, 0.5); - bMaterial.emissiveTexture = null; - } else { - bMaterial.emissiveColor = Color.toBabylon(Color.BLACK); - bMaterial.emissiveTexture = new Texture(emissive.next.uri, this.bScene_); - } - break; - } - } - } - - if (metalness.type === Patch.Type.OuterChange) { - switch (metalness.next.type) { - case 'color1': { - bMaterial.metallic = metalness.next.color; - bMaterial.metallicTexture = null; - break; - } - case 'texture': { - if (!metalness.next.uri) { - bMaterial.metallic = 0; - } else { - bMaterial.metallicTexture = new Texture(metalness.next.uri, this.bScene_); - } - break; - } - } - } - - if (reflection.type === Patch.Type.OuterChange) { - switch (reflection.next.type) { - case 'color3': { - bMaterial.reflectivityColor = Color.toBabylon(reflection.next.color); - bMaterial.reflectivityTexture = null; - break; - } - case 'texture': { - if (!reflection.next.uri) { - bMaterial.reflectivityColor = new Color3(0.5, 0, 0.5); - bMaterial.reflectivityTexture = null; - } else { - bMaterial.reflectivityColor = Color.toBabylon(Color.WHITE); - bMaterial.reflectivityTexture = new Texture(reflection.next.uri, this.bScene_); - } - break; - } - } - } - - return bMaterial; - }; - + private updateMaterial_ = (bMaterial: babylMaterial, material: Patch<Material>) => { switch (material.type) { case Patch.Type.OuterChange: { @@ -549,7 +158,7 @@ class SceneBinding { const id = bMaterial ? `${bMaterial.id}` : `Scene Material ${this.materialIdIter_++}`; if (bMaterial) bMaterial.dispose(); if (next) { - return this.createMaterial_(id, next); + return createMaterial(id, next, this.bScene_); } return null; } @@ -557,10 +166,10 @@ class SceneBinding { const { inner, next } = material; switch (next.type) { case 'basic': { - return this.updateMaterialBasic_(bMaterial as StandardMaterial, inner as Patch.InnerPatch<Material.Basic>); + return updateMaterialBasic(bMaterial as StandardMaterial, inner as Patch.InnerPatch<Material.Basic>, this.bScene_); } case 'pbr': { - return this.updateMaterialPbr_(bMaterial as PBRMaterial, inner as Patch.InnerPatch<Material.Pbr>); + return updateMaterialPbr(bMaterial as PBRMaterial, inner as Patch.InnerPatch<Material.Pbr>, this.bScene_); } } break; @@ -570,86 +179,6 @@ class SceneBinding { return bMaterial; }; - private createObject_ = async (node: Node.Obj, nextScene: Scene): Promise<babylNode> => { - const parent = this.findBNode_(node.parentId, true); - - const geometry = nextScene.geometry[node.geometryId] ?? preBuiltGeometries[node.geometryId]; - if (!geometry) { - console.error(`node ${LocalizedString.lookup(node.name, LocalizedString.EN_US)} has invalid geometry ID: ${node.geometryId}`); - return null; - } - const ret = await this.buildGeometry_(node.name[LocalizedString.EN_US], geometry, node.faceUvs); - - if (!node.visible) { - SceneBinding.apply_(ret, m => m.isVisible = false); - } - - if (node.material) { - const material = this.createMaterial_(node.name[LocalizedString.EN_US], node.material); - SceneBinding.apply_(ret, m => m.material = material); - } - - ret.setParent(parent); - return ret; - }; - - private createEmpty_ = (node: Node.Empty): TransformNode => { - const parent = this.findBNode_(node.parentId, true); - - const ret = new TransformNode(node.name[LocalizedString.EN_US], this.bScene_); - ret.setParent(parent); - return ret; - }; - - private createDirectionalLight_ = (id: string, node: Node.DirectionalLight): DirectionalLight => { - const ret = new DirectionalLight(node.name[LocalizedString.EN_US], RawVector3.toBabylon(node.direction), this.bScene_); - - ret.intensity = node.intensity; - if (node.radius !== undefined) ret.radius = node.radius; - if (node.range !== undefined) ret.range = node.range; - - this.shadowGenerators_[id] = SceneBinding.createShadowGenerator_(ret); - - return ret; - }; - - private createSpotLight_ = (id: string, node: Node.SpotLight): SpotLight => { - const origin: ReferenceFramewUnits = node.origin ?? {}; - const position: Vector3wUnits = origin.position ?? Vector3wUnits.zero(); - - const ret = new SpotLight( - node.name[LocalizedString.EN_US], - RawVector3.toBabylon(Vector3wUnits.toRaw(position, 'centimeters')), - RawVector3.toBabylon(node.direction), - Angle.toRadiansValue(node.angle), - node.exponent, - this.bScene_ - ); - - this.shadowGenerators_[id] = SceneBinding.createShadowGenerator_(ret); - - return ret; - }; - - private createPointLight_ = (id: string, node: Node.PointLight): PointLight => { - const origin: ReferenceFramewUnits = node.origin ?? {}; - const position: Vector3wUnits = origin.position ?? Vector3wUnits.zero(); - - const ret = new PointLight( - node.name[LocalizedString.EN_US], - RawVector3.toBabylon(Vector3wUnits.toRaw(position, 'centimeters')), - this.bScene_ - ); - - ret.intensity = node.intensity; - - this.shadowGenerators_[id] = SceneBinding.createShadowGenerator_(ret); - - ret.setEnabled(node.visible); - - return ret; - }; - // Create Robot Binding private createRobot_ = async (id: string, node: Node.Robot): Promise<RobotBinding> => { // This should probably be somewhere else, but it ensures this is called during @@ -657,12 +186,11 @@ class SceneBinding { WorkerInstance.sync(node.state); const robotBinding = new RobotBinding(this.bScene_, this.physicsViewer_); + const robot = this.robots_[node.robotId]; if (!robot) throw new Error(`Robot by id "${node.robotId}" not found`); await robotBinding.setRobot(node, robot, id); robotBinding.linkOrigins = this.robotLinkOrigins_[id] || {}; - // console.log('robot linkOrigins', robotBinding.linkOrigins); - // Here the linkOrigins are all shown as 0,0,0, this may be why the initial kinematics are messed up. robotBinding.visible = true; const observerObj: { observer: Observer<babylScene> } = { observer: null }; @@ -695,14 +223,6 @@ class SceneBinding { return robotBinding; }; - private static createShadowGenerator_ = (light: IShadowLight) => { - const ret = new ShadowGenerator(1024, light); - ret.useKernelBlur = false; - ret.blurScale = 2; - ret.filter = ShadowGenerator.FILTER_POISSONSAMPLING; - return ret; - }; - private createNode_ = async (id: string, node: Node, nextScene: Scene): Promise<babylNode> => { let nodeToCreate: Node = node; @@ -724,11 +244,17 @@ class SceneBinding { let ret: babylNode; switch (nodeToCreate.type) { - case 'object': ret = await this.createObject_(nodeToCreate, nextScene); break; - case 'empty': ret = this.createEmpty_(nodeToCreate); break; - case 'directional-light': ret = this.createDirectionalLight_(id, nodeToCreate); break; - case 'spot-light': ret = this.createSpotLight_(id, nodeToCreate); break; - case 'point-light': ret = this.createPointLight_(id, nodeToCreate); break; + case 'object': { + const parent = this.findBNode_(nodeToCreate.parentId, true); + ret = await createObject(nodeToCreate, nextScene, parent, this.bScene_); break; + } + case 'empty': { + const parent = this.findBNode_(nodeToCreate.parentId, true); + ret = createEmpty(nodeToCreate, parent, this.bScene_); break; + } + case 'directional-light': ret = createDirectionalLight(id, nodeToCreate, this.bScene_, this.shadowGenerators_); break; + case 'spot-light': ret = createSpotLight(id, nodeToCreate, this.bScene_, this.shadowGenerators_); break; + case 'point-light': ret = createPointLight(id, nodeToCreate, this.bScene_, this.shadowGenerators_); break; case 'robot': await this.createRobot_(id, nodeToCreate); break; default: { console.warn('invalid node type for create node:', nodeToCreate.type); @@ -744,7 +270,7 @@ class SceneBinding { ret.metadata = { id } as SceneMeshMetadata; if (ret instanceof AbstractMesh || ret instanceof TransformNode) { - SceneBinding.apply_(ret, m => { + apply(ret, m => { m.metadata = { id } as SceneMeshMetadata; this.restorePhysicsToObject(m, nodeToCreate as Node.Obj, null, nextScene); }); @@ -824,7 +350,7 @@ class SceneBinding { let bMaterial = this.findMaterial_(bNode); bMaterial = this.updateMaterial_(bMaterial, node.inner.material); - SceneBinding.apply_(bNode, m => { + apply(bNode, m => { m.material = bMaterial; }); @@ -834,7 +360,7 @@ class SceneBinding { } if (node.inner.physics.type === Patch.Type.OuterChange) { - SceneBinding.apply_(bNode, m => { + apply(bNode, m => { this.removePhysicsFromObject(m); this.restorePhysicsToObject(m, node.next, id, nextScene); }); @@ -842,7 +368,7 @@ class SceneBinding { if (node.inner.visible.type === Patch.Type.OuterChange) { const nextVisible = node.inner.visible.next; - SceneBinding.apply_(bNode, m => { + apply(bNode, m => { m.isVisible = nextVisible; // Create/remove physics for object becoming visible/invisible @@ -1093,29 +619,6 @@ class SceneBinding { private gizmoManager_: GizmoManager; - private createArcRotateCamera_ = (camera: Camera.ArcRotate): ArcRotateCamera => { - const ret = new ArcRotateCamera('botcam', 0, 0, 0, Vector3wUnits.toBabylon(camera.target, 'centimeters'), this.bScene_); - ret.attachControl(this.bScene_.getEngine().getRenderingCanvas(), true); - ret.position = Vector3wUnits.toBabylon(camera.position, 'centimeters'); - ret.panningSensibility = 100; - // ret.checkCollisions = true; - - return ret; - }; - - private createNoneCamera_ = (camera: Camera.None): ArcRotateCamera => { - const ret = new ArcRotateCamera('botcam', 10, 10, 10, Vector3wUnits.toBabylon(Vector3wUnits.zero(), 'centimeters'), this.bScene_); - ret.attachControl(this.bScene_.getEngine().getRenderingCanvas(), true); - - return ret; - }; - - private createCamera_ = (camera: Camera): babylCamera => { - switch (camera.type) { - case 'arc-rotate': return this.createArcRotateCamera_(camera); - case 'none': return this.createNoneCamera_(camera); - } - }; private updateArcRotateCamera_ = (node: Patch.InnerChange<Camera.ArcRotate>): ArcRotateCamera => { if (!(this.camera_ instanceof ArcRotateCamera)) throw new Error('Expected ArcRotateCamera'); @@ -1315,7 +818,7 @@ class SceneBinding { const prevBNode = this.bScene_.getNodeById(prev); if (prevNodeObj && (prevBNode instanceof AbstractMesh || prevBNode instanceof TransformNode)) { prevBNode.metadata = { ...(prevBNode.metadata as SceneMeshMetadata), selected: false }; - SceneBinding.apply_(prevBNode, m => this.restorePhysicsToObject(m, prevNodeObj, prev, scene)); + apply(prevBNode, m => this.restorePhysicsToObject(m, prevNodeObj, prev, scene)); } this.gizmoManager_.attachToNode(null); @@ -1325,7 +828,7 @@ class SceneBinding { if (next !== undefined) { const node = this.bScene_.getNodeById(next); if (node instanceof AbstractMesh || node instanceof TransformNode) { - SceneBinding.apply_(node, m => this.removePhysicsFromObject(m)); + apply(node, m => this.removePhysicsFromObject(m)); node.metadata = { ...(node.metadata as SceneMeshMetadata), selected: true }; this.gizmoManager_.attachToNode(node); } @@ -1335,12 +838,12 @@ class SceneBinding { const oldCamera = this.camera_; switch (patch.camera.type) { case Patch.Type.OuterChange: { - this.camera_ = this.createCamera_(patch.camera.next); + this.camera_ = createCamera(patch.camera.next, this.bScene_); break; } case Patch.Type.InnerChange: { if (this.camera_) this.camera_ = this.updateCamera_(patch.camera); - else this.camera_ = this.createCamera_(patch.camera.next); + else this.camera_ = createCamera(patch.camera.next, this.bScene_); break; } } diff --git a/src/Bindings/WeightBinding.ts b/src/Bindings/WeightBinding.ts new file mode 100644 index 00000000..13faa2fb --- /dev/null +++ b/src/Bindings/WeightBinding.ts @@ -0,0 +1,44 @@ + +import { Scene as babylScene, CreateSphere, Vector3, Mesh, + PhysicsAggregate, PhysicsShapeType, LockConstraint, +} from '@babylonjs/core'; + +import Node from '../state/State/Robot/Node'; +import { ReferenceFramewUnits } from '../util/unit-math'; +import { RENDER_SCALE } from '../components/Constants/renderConstants'; +import { Mass } from '../util/Value'; +import Robot from '../state/State/Robot'; +import Dict from '../util/Dict'; + +// Adds an invisible weight to a parent link. +export const createWeight_ = (id: string, weight: Node.Weight, bScene_: babylScene, robot_: Robot, links_: Dict<Mesh>) => { + const ret = CreateSphere(id, { diameter: 1 }, bScene_); + ret.visibility = 0; + + const parent = robot_.nodes[weight.parentId]; + if (!parent) throw new Error(`Missing parent: "${weight.parentId}" for weight "${id}"`); + if (parent.type !== Node.Type.Link) throw new Error(`Invalid parent type: "${parent.type}" for weight "${id}"`); + + const aggregate = new PhysicsAggregate(ret, PhysicsShapeType.CYLINDER, { + mass: Mass.toGramsValue(weight.mass), + friction: 0, + restitution: 0, + }, bScene_); + + const bParent = links_[weight.parentId]; + if (!bParent) throw new Error(`Missing parent instantiation: "${weight.parentId}" for weight "${id}"`); + + const bOrigin = ReferenceFramewUnits.toBabylon(weight.origin, RENDER_SCALE); + + const constraint = new LockConstraint( + bOrigin.position, + new Vector3(0,0,0), // RawVector3.toBabylon(new Vector3(-8,10,0)), // updown, frontback + Vector3.Up(), + Vector3.Up().applyRotationQuaternion(bOrigin.rotationQuaternion.invert()), + bScene_ + ); + + bParent.physicsBody.addConstraint(ret.physicsBody, constraint); + + return ret; +}; \ No newline at end of file diff --git a/src/Bindings/helpers.ts b/src/Bindings/helpers.ts new file mode 100644 index 00000000..34aa13a8 --- /dev/null +++ b/src/Bindings/helpers.ts @@ -0,0 +1,16 @@ + +import { PhysicsShapeType, IPhysicsCollisionEvent, IPhysicsEnginePluginV2, PhysicsAggregate, + TransformNode, AbstractMesh, PhysicsViewer, ShadowGenerator, CreateBox, CreateSphere, CreateCylinder, + CreatePlane, Vector4, Vector3, Texture, DynamicTexture, StandardMaterial, GizmoManager, ArcRotateCamera, + IShadowLight, PointLight, SpotLight, DirectionalLight, Color3, PBRMaterial, Mesh, SceneLoader, EngineView, + Scene as babylScene, Node as babylNode, Camera as babylCamera, Material as babylMaterial, + GlowLayer, Observer, BoundingBox } from '@babylonjs/core'; + + +export const apply = (g: babylNode, f: (m: AbstractMesh) => void) => { + if (g instanceof AbstractMesh) { + f(g); + } else { + (g.getChildren(c => c instanceof AbstractMesh) as AbstractMesh[]).forEach(f); + } +}; \ No newline at end of file diff --git a/src/Sim.tsx b/src/Sim.tsx index ae0da088..172816b2 100644 --- a/src/Sim.tsx +++ b/src/Sim.tsx @@ -16,7 +16,7 @@ import { Angle } from './util'; import store from './state'; import { Unsubscribe } from 'redux'; import { ScenesAction } from './state/reducer'; -import SceneBinding, { SceneMeshMetadata } from './SceneBinding'; +import SceneBinding, { SceneMeshMetadata } from './Bindings/SceneBinding'; import Scene from './state/State/Scene'; import Node from './state/State/Scene/Node'; import { Robots } from './state/State'; diff --git a/src/robots/demobot.ts b/src/robots/demobot.ts index 85e1551e..7b8ea70a 100644 --- a/src/robots/demobot.ts +++ b/src/robots/demobot.ts @@ -21,6 +21,7 @@ export const DEMOBOT: Robot = { mass: grams(400), restitution: 0, friction: 0.01, + inertia: [10, 10, 10] }), lightSensor: Node.lightSensor({ parentId: 'chassis', @@ -83,6 +84,7 @@ export const DEMOBOT: Robot = { mass: grams(14), friction: 50, restitution: 0, + inertia: [6, 6, 6], collisionBody: Node.Link.CollisionBody.EMBEDDED, }), claw: Node.servo({ @@ -99,6 +101,7 @@ export const DEMOBOT: Robot = { mass: grams(7), friction: 50, restitution: 0, + inertia: [3, 3, 3], collisionBody: Node.Link.CollisionBody.EMBEDDED, }), touch_sensor: Node.touchSensor({