Closed IRobot1 closed 2 years ago
One solution might be where the ngtPhysicConeTwistConstraint supports bodyAuuid and bodyB.uuid inputs that allow the two physics body to be declared, that might work. For example
<ngt-mesh #head="ngtPhysicSphere" ngtPhysicSphere [getPhysicProps]="getHeadProps">
<ngt-sphere-geometry [args]="[headRadius]"></ngt-sphere-geometry>
</ngt-mesh>
<ngt-mesh #upperBody="ngtPhysicBox" ngtPhysicBox [getPhysicProps]="getUpperBodyProps">
<ngt-box-geometry [args]="upperBodyArgs"></ngt-box-geometry>
</ngt-mesh>
<ng-container ngtPhysicConeTwistConstraint [options]="neckjoint" [bodyA]="head" [bodyB]="upperBody">
</ng-container>
Given this, perhaps using a dedicated ngt-conetwist-constraint element would make more sense than using ng-container.
<ngt-conetwist-constraint [bodyA]="head" [bodyB]="upperBody" [options]="neckjoint">
</ngt-conetwist-constraint>
Thanks for exploring these! I'll try the ragdoll example from React Three tonight to see if we need to adjust the API of constraints somehow to make it work better in Angular. Once again, extremely appreciate your efforts!
@IRobot1 The React Three version (https://cannon.pmnd.rs/) recursively references the parent context to grab the parent Object3D. I think we can do the same for Angular (so no *ngFor
). I'll explore more today if I have time.
I tried creating a wrapper component that was recursive. It was passed the array of things to connect and the current index. Consider a component call 'conetwist-link'
<ng-container ngtXXXConstraint>
<ngt-mesh ngtPhysicXXX>
</ngt-mesh>
<conetwist-link *ngIf="index < constraints.length-1" [constraints]="constraints" [index]="index+1"></conetwist-link>
</ng-container>
I wasn't successful. The outer container (bodyA) doesn't see the mesh in the nested container (bodyB).
Without a good example, this will never be obvious on how to implement.
I still think declaring the constraints separate has merit. It would be easy to use with ngFor. ngIf would allow the constraint to be removed (arm to fall off or chain to split).
I think that letting the developer decide how the constraints are applied is more flexible than doing it automatically.
One use case would be a link of objects connected with constraints, but having the first connected to the last. For having multiple chains connected to a central point (hanging lights).
@IRobot1 I agree. How would the public API look like?
You mean something like this? @Input() bodyA: NgtPhysicBase - some type of base class @Input() bodyB: NgtPhysicBase - some type of base class @Input() options: Record<string, unknown>
api() : { methods like you have now } There could either be separate ngt-|type|-constraint elements or type could be an @Input. The problem with having Input is that it would support changing type which would not be good.
@IRobot1 After hours of pain last night trying out Ragdoll with Angular Three, I think the current implementation that use Directives aren't enough. I'll switch to Service-based Physic to see if it improves things.
@Component({
template: `
<ngt-mesh [objectRef]="boxRef"></ngt-mesh>
`
})
export class SomeComponent {
boxRef = this.physicBody.useBox(/* anything that box accepts */)
constructor(private physicBody: NgtPhysicBody) {}
}
Something like this. But this requires me to change the underlying objects' wrappers to use ref
instead (you can think of ref
as I can make a Ref here and pass it around, updating to the Ref will make sure ALL the places that use the Ref get the changes)
Sounds good. Sorry to cause a re-design. Take your time.
When you're ready, I can quickly convert the examples to your new design to help you valid all is good.
@IRobot1 I got the ref to work with Bodies.
import { NgtPhysicsModule } from '@angular-three/cannon';
import { NgtPhysicBody } from '@angular-three/cannon/bodies';
import { NgtCannonDebugModule } from '@angular-three/cannon/debug';
import {
NgtCanvasModule,
NgtColorPipeModule,
NgtEuler,
NgtTriple,
NgtVector3,
} from '@angular-three/core';
import {
NgtColorAttributeModule,
NgtVector2AttributeModule,
} from '@angular-three/core/attributes';
import {
NgtBoxGeometryModule,
NgtPlaneGeometryModule,
} from '@angular-three/core/geometries';
import {
NgtAmbientLightModule,
NgtDirectionalLightModule,
} from '@angular-three/core/lights';
import {
NgtMeshLambertMaterialModule,
NgtShadowMaterialModule,
} from '@angular-three/core/materials';
import { NgtMeshModule } from '@angular-three/core/meshes';
import {
ChangeDetectionStrategy,
Component,
Input,
NgModule,
} from '@angular/core';
@Component({
selector: 'sandbox-physic-cubes',
template: `
<ngt-canvas
initialLog
shadows
[dpr]="[1, 2]"
[gl]="{ alpha: false }"
[camera]="{ position: [-1, 5, 5], fov: 45 }"
>
<ngt-color attach="background" color="lightblue"></ngt-color>
<ngt-ambient-light></ngt-ambient-light>
<ngt-directional-light [position]="10" castShadow>
<ngt-vector2
[attach]="['shadow', 'mapSize']"
[vector2]="2048"
></ngt-vector2>
</ngt-directional-light>
<ngt-physics>
<sandbox-plane [position]="[0, -2.5, 0]"></sandbox-plane>
<sandbox-cube [position]="[0.1, 5, 0]"></sandbox-cube>
<sandbox-cube [position]="[0, 10, -1]"></sandbox-cube>
<sandbox-cube [position]="[0, 20, -2]"></sandbox-cube>
</ngt-physics>
</ngt-canvas>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SandboxPhysicCubesComponent {}
@Component({
selector: 'sandbox-plane',
template: `
<!-- 👇 -->
<ngt-mesh receiveShadow [ref]="planeRef.ref">
<ngt-plane-geometry [args]="[1000, 1000]"></ngt-plane-geometry>
<ngt-shadow-material
color="#171717"
[transparent]="true"
[opacity]="0.4"
></ngt-shadow-material>
</ngt-mesh>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [NgtPhysicBody],
})
export class SandboxPlaneComponent {
@Input() position?: NgtVector3;
rotation = [-Math.PI / 2, 0, 0] as NgtEuler;
// 👇
planeRef = this.physicBody.usePlane(() => ({
args: [1000, 1000],
rotation: this.rotation as NgtTriple,
position: this.position as NgtTriple,
}));
constructor(private physicBody: NgtPhysicBody) {}
}
@Component({
selector: 'sandbox-cube',
template: `
<!-- 👇 -->
<ngt-mesh receiveShadow castShadow [ref]="boxRef.ref">
<ngt-box-geometry></ngt-box-geometry>
<ngt-mesh-lambert-material
color="hotpink"
></ngt-mesh-lambert-material>
</ngt-mesh>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [NgtPhysicBody],
})
export class SandboxCubeComponent {
@Input() position?: NgtVector3;
rotation = [0.4, 0.2, 0.5] as NgtEuler;
// 👇
boxRef = this.physicBody.useBox(() => ({
mass: 1,
position: this.position as NgtTriple,
rotation: this.rotation as NgtTriple,
}));
constructor(private physicBody: NgtPhysicBody) {}
}
@NgModule({
declarations: [
SandboxPhysicCubesComponent,
SandboxPlaneComponent,
SandboxCubeComponent,
],
exports: [SandboxPhysicCubesComponent, SandboxPlaneComponent],
imports: [
NgtCanvasModule,
NgtColorAttributeModule,
NgtAmbientLightModule,
NgtDirectionalLightModule,
NgtVector2AttributeModule,
NgtPhysicsModule,
NgtMeshModule,
NgtPlaneGeometryModule,
NgtShadowMaterialModule,
NgtBoxGeometryModule,
NgtMeshLambertMaterialModule,
NgtCannonDebugModule,
NgtColorPipeModule,
],
})
export class SandboxPhysicCubesModule {}
@IRobot1 good news. Got Constraint to work https://www.youtube.com/watch?v=fab5itCc-2A
The code is quite long and it's not available on Github yet (It's on my local branch with some breaking changes which I'll be releasing as the next major version for Angular Three)
import {
ConeTwistConstraintOpts,
NgtPhysicsModule,
} from '@angular-three/cannon';
import {
NgtPhysicBody,
NgtPhysicsBodyPublicApi,
} from '@angular-three/cannon/bodies';
import {
NgtConstraintReturn,
NgtPhysicConstraint,
} from '@angular-three/cannon/constraints';
import {
NgtCanvasModule,
NgtObject,
NgtRenderState,
NgtTriple,
NgtVector3,
Ref,
} from '@angular-three/core';
import {
NgtColorAttributeModule,
NgtFogAttributeModule,
NgtVector3AttributeModule,
} from '@angular-three/core/attributes';
import {
NgtBoxGeometryModule,
NgtPlaneGeometryModule,
NgtSphereGeometryModule,
} from '@angular-three/core/geometries';
import { NgtGroupModule } from '@angular-three/core/group';
import {
NgtAmbientLightModule,
NgtPointLightModule,
} from '@angular-three/core/lights';
import {
NgtMeshBasicMaterialModule,
NgtMeshStandardMaterialModule,
} from '@angular-three/core/materials';
import { NgtMeshModule } from '@angular-three/core/meshes';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
ContentChild,
Directive,
Input,
NgModule,
OnInit,
Optional,
Self,
SkipSelf,
TemplateRef,
} from '@angular/core';
import { takeUntil } from 'rxjs';
import * as THREE from 'three';
import { createRagdoll, ShapeConfig, ShapeName } from './monday-morning.config';
const { joints, shapes } = createRagdoll(4.8, Math.PI / 16, Math.PI / 16, 0);
const double = ([x, y, z]: Readonly<NgtTriple>): NgtTriple => [
x * 2,
y * 2,
z * 2,
];
const cursor = new Ref<THREE.Object3D>();
@Component({
selector: 'sandbox-monday-morning',
template: `
<ngt-canvas
[camera]="{ far: 100, near: 1, position: [-25, 20, 25], zoom: 25 }"
orthographic
shadows
style="cursor: none"
initialLog
>
<ngt-color attach="background" color="#171720"></ngt-color>
<ngt-fog attach="fog" [fog]="['#171720', 20, 70]"></ngt-fog>
<ngt-ambient-light [intensity]="0.2"></ngt-ambient-light>
<ngt-point-light
[position]="[-10, -10, -10]"
color="red"
[intensity]="1.5"
></ngt-point-light>
<ngt-physics
[iterations]="15"
[gravity]="[0, -200, 0]"
[allowSleep]="false"
>
<sandbox-cursor></sandbox-cursor>
<sandbox-ragdoll [position]="[0, 0, 0]"></sandbox-ragdoll>
<sandbox-plane></sandbox-plane>
</ngt-physics>
<!-- <Physics iterations={15} gravity={[0, -200, 0]} allowSleep={false}>-->
<!-- <Cursor />-->
<!-- <Ragdoll position={[0, 0, 0]} />-->
<!-- <Plane position={[0, -5, 0]} rotation={[-Math.PI / 2, 0, 0]} />-->
<!-- <Chair />-->
<!-- <Table />-->
<!-- <Lamp />-->
<!-- </Physics>-->
</ngt-canvas>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SandboxMondayMorningComponent {}
@Component({
selector: 'sandbox-cursor',
template: `
<ngt-mesh
[ref]="sphereRef.ref"
(beforeRender)="onCursorBeforeRender($event.state)"
>
<ngt-sphere-geometry [args]="[0.5, 32, 32]"></ngt-sphere-geometry>
<ngt-mesh-basic-material
[fog]="false"
[depthTest]="false"
[transparent]="true"
[opacity]="0.5"
></ngt-mesh-basic-material>
</ngt-mesh>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [NgtPhysicBody],
})
export class SandboxCursorComponent {
sphereRef = this.physicBody.useSphere(
() => ({
args: [0.5],
position: [0, 0, 10000],
type: 'Static',
}),
cursor
);
constructor(private physicBody: NgtPhysicBody) {}
onCursorBeforeRender({
pointer,
viewport: { width, height },
}: NgtRenderState) {
const x = pointer.x * width;
const y = (pointer.y * height) / 1.9 + -x / 3.5;
this.sphereRef.api.position.set(x / 1.4, y, 0);
}
}
@Directive({
selector: '[sandboxDragConstraint]',
providers: [NgtPhysicConstraint],
})
export class SandboxDragConstraintDirective implements OnInit {
private constraint!: NgtConstraintReturn<'PointToPoint'>;
constructor(
private physicConstraint: NgtPhysicConstraint,
@Self()
private object: NgtObject
) {
object.pointerdown
.pipe(takeUntil(object.destroy$))
.subscribe((event: any) => {
event.stopPropagation();
event.target.setPointerCapture(event.pointerId);
this.constraint.api.enable();
});
object.pointerup.pipe(takeUntil(object.destroy$)).subscribe(() => {
this.constraint.api.disable();
});
}
ngOnInit() {
this.constraint = this.physicConstraint.usePointToPointConstraint(
cursor,
this.object.instance as unknown as Ref<THREE.Object3D>,
{ pivotA: [0, 0, 0], pivotB: [0, 0, 0] }
);
this.constraint.api.disable();
}
}
@Component({
selector: 'sandbox-box[name]',
template: `
<ng-container *ngIf="boxRef">
<ngt-mesh
castShadow
receiveShadow
sandboxDragConstraint
[ref]="boxRef.ref"
[name]="name"
[position]="position"
>
<ngt-vector3 attach="scale" [vector3]="scale"></ngt-vector3>
<ngt-box-geometry [args]="args"></ngt-box-geometry>
<ngt-mesh-standard-material
[color]="shape.color"
[opacity]="opacity"
[transparent]="transparent"
></ngt-mesh-standard-material>
<ng-container
*ngIf="renderTemplate && name !== 'upperBody'"
[ngTemplateOutlet]="renderTemplate"
[ngTemplateOutletContext]="{ scale, parent: boxRef.ref }"
></ng-container>
</ngt-mesh>
<ng-container
*ngIf="childTemplate"
[ngTemplateOutlet]="childTemplate"
></ng-container>
<ng-content></ng-content>
</ng-container>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [NgtPhysicBody, NgtPhysicConstraint],
})
export class SandboxBoxComponent implements OnInit {
@Input() position?: NgtVector3;
@Input() args: ConstructorParameters<typeof THREE.BoxGeometry> = [1, 1, 1];
@Input() opacity = 1;
@Input() transparent = false;
@Input() name!: ShapeName;
@Input() config: ConeTwistConstraintOpts = {};
@ContentChild('child', { read: TemplateRef })
childTemplate?: TemplateRef<unknown>;
@ContentChild('render', { read: TemplateRef })
renderTemplate?: TemplateRef<unknown>;
shape!: ShapeConfig;
scale!: NgtTriple;
boxRef!: { ref: Ref<THREE.Object3D>; api: NgtPhysicsBodyPublicApi };
constructor(
@Optional()
@SkipSelf()
private parentBox: SandboxBoxComponent,
private physicBody: NgtPhysicBody,
private physicConstraint: NgtPhysicConstraint
) {}
ngOnInit() {
this.shape = shapes[this.name];
this.scale = double(this.shape.args);
this.boxRef = this.physicBody.useBox(() => ({
args: [...this.shape.args],
linearDamping: 0.99,
mass: this.shape.mass,
position: [...this.shape.position],
}));
if (this.parentBox) {
this.physicConstraint.useConeTwistConstraint(
this.boxRef.ref,
this.parentBox.boxRef.ref,
this.config
);
}
}
}
@Component({
selector: 'sandbox-ragdoll',
template: `
<sandbox-box [position]="position" name="upperBody">
<ng-template #child>
<sandbox-box
name="head"
[position]="position"
[config]="joints['neckJoint']"
>
<ng-template #render let-scale="scale" let-parent="parent">
<ngt-group
(beforeRender)="onEyesBeforeRender($event)"
[appendTo]="parent"
>
<ngt-mesh
castShadow
receiveShadow
[position]="[-0.3, 0.1, 0.5]"
>
<ngt-vector3
attach="scale"
[vector3]="scale"
></ngt-vector3>
<ngt-box-geometry
[args]="[0.3, 0.01, 0.1]"
></ngt-box-geometry>
<ngt-mesh-standard-material
color="black"
[opacity]="0.8"
[transparent]="true"
></ngt-mesh-standard-material>
</ngt-mesh>
<ngt-mesh
castShadow
receiveShadow
[position]="[0.3, 0.1, 0.5]"
>
<ngt-vector3
attach="scale"
[vector3]="scale"
></ngt-vector3>
<ngt-box-geometry
[args]="[0.3, 0.01, 0.1]"
></ngt-box-geometry>
<ngt-mesh-standard-material
color="black"
[opacity]="0.8"
[transparent]="true"
></ngt-mesh-standard-material>
</ngt-mesh>
</ngt-group>
<ngt-mesh
castShadow
receiveShadow
[position]="[0, -0.2, 0.5]"
(beforeRender)="onMouthBeforeRender($event)"
[appendTo]="parent"
>
<ngt-vector3
attach="scale"
[vector3]="scale"
></ngt-vector3>
<ngt-box-geometry
[args]="[0.3, 0.05, 0.1]"
></ngt-box-geometry>
<ngt-mesh-standard-material
color="black"
[opacity]="0.8"
[transparent]="true"
></ngt-mesh-standard-material>
</ngt-mesh>
</ng-template>
</sandbox-box>
<sandbox-box
name="upperLeftArm"
[config]="joints['leftShoulder']"
[position]="position"
>
<ng-template #child>
<sandbox-box
[position]="position"
name="lowerLeftArm"
[config]="joints['leftElbowJoint']"
></sandbox-box>
</ng-template>
</sandbox-box>
<sandbox-box
name="upperRightArm"
[config]="joints['rightShoulder']"
[position]="position"
>
<ng-template #child>
<sandbox-box
[position]="position"
name="lowerRightArm"
[config]="joints['rightElbowJoint']"
></sandbox-box>
</ng-template>
</sandbox-box>
<sandbox-box
name="pelvis"
[config]="joints['spineJoint']"
[position]="position"
>
<ng-template #child>
<sandbox-box
name="upperLeftLeg"
[config]="joints['leftHipJoint']"
[position]="position"
>
<ng-template #child>
<sandbox-box
name="lowerLeftLeg"
[config]="joints['leftKneeJoint']"
[position]="position"
></sandbox-box>
</ng-template>
</sandbox-box>
<sandbox-box
name="upperRightLeg"
[config]="joints['rightHipJoint']"
[position]="position"
>
<ng-template #child>
<sandbox-box
name="lowerRightLeg"
[config]="joints['rightKneeJoint']"
[position]="position"
></sandbox-box>
</ng-template>
</sandbox-box>
</ng-template>
</sandbox-box>
</ng-template>
</sandbox-box>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SandboxRagdollComponent {
@Input() position?: NgtVector3;
readonly joints = joints;
onEyesBeforeRender({
object,
state: { clock },
}: {
state: NgtRenderState;
object: THREE.Group;
}) {
object.position.y = Math.sin(clock.getElapsedTime()) * 0.06;
}
onMouthBeforeRender({
object,
state: { clock },
}: {
state: NgtRenderState;
object: THREE.Mesh;
}) {
object.scale.y = (1 + Math.sin(clock.getElapsedTime())) * 1.5;
}
}
@Component({
selector: 'sandbox-plane',
template: `
<ngt-mesh receiveShadow [ref]="planeRef.ref">
<ngt-plane-geometry [args]="[1000, 1000]"></ngt-plane-geometry>
<ngt-mesh-standard-material
color="#171720"
></ngt-mesh-standard-material>
</ngt-mesh>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [NgtPhysicBody],
})
export class SandboxPlaneComponent {
planeRef = this.physicBody.usePlane(() => ({
args: [1000, 1000],
position: [0, -5, 0],
rotation: [-Math.PI / 2, 0, 0],
}));
constructor(private physicBody: NgtPhysicBody) {}
}
@NgModule({
declarations: [
SandboxMondayMorningComponent,
SandboxCursorComponent,
SandboxBoxComponent,
SandboxRagdollComponent,
SandboxPlaneComponent,
SandboxDragConstraintDirective,
],
exports: [SandboxMondayMorningComponent, SandboxBoxComponent],
imports: [
NgtMeshModule,
NgtBoxGeometryModule,
NgtMeshStandardMaterialModule,
CommonModule,
NgtGroupModule,
NgtVector3AttributeModule,
NgtPlaneGeometryModule,
NgtCanvasModule,
NgtColorAttributeModule,
NgtFogAttributeModule,
NgtAmbientLightModule,
NgtPointLightModule,
NgtPhysicsModule,
NgtSphereGeometryModule,
NgtMeshBasicMaterialModule,
],
})
export class SandboxMondayMorningModule {}
Nice work. That approach looks much cleaner and more flexible.
@IRobot1 Thanks. If you'd like, I can try publishing beta version so you can play with the new API. Currently, I can't because soba
and postprocessing
are broken w/ the breaking changes :D
I'd be happy to give it a try
Fixed in version 5 when its released. See constraints example
I'm converting the cannon-es ragdoll example
The example connects multiple meshes together using a cone twist constraint
The current ngtPhysicConeTwistConstraint requires both meshes to be in the same ng-container. However, given how things are connected in this example, its no possible using the currently solution.
Here's what I am able to complete so far. The ragdoll on the left shows arms, legs and head connected, but not connected to the body parts. The ragdoll on the right is static showing how the parts should be connected.
Partially working code is here. Pick ragdoll