Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Joints implementation #150

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
7 changes: 6 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,10 @@
},
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
}
},

"cSpell.words": [
JaimeTorrealba marked this conversation as resolved.
Show resolved Hide resolved
"cientos",
"tresjs"
]
}
66 changes: 66 additions & 0 deletions docs/components/joint.md
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Neosoulink you need to add the entry on docs/.vitepress/config so it's available on the side navigation

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted

Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Joints

Joints is an extension feature provided in [Rapier#Joint](https://rapier.rs/docs/user_guides/javascript/joints/). It lets us connect two or more bodies, restricting their movements according to each other.

In **Tres** we can achieve such motion restriction by using one of the available components designed to handle joints:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small one but maybe we need consistency here it all pages should be "Tres.js"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted


- [GenericJoint](../../src/components/joints/index.ts#L41)
- [PrismaticJoint](../../src/components/joints/index.ts#L51)
- [RevoluteJoint](../../src/components/joints/index.ts#L60)
- [RopeJoint](../../src/components/joints/index.ts#L69)
- [SphericalJoint](../../src/components/joints/index.ts#L78)
- [SpringJoint](../../src/components/joints/index.ts#L86)

All of them extends [BaseJoint](../../src/components/joints/index.ts#L96).

## How to use

Here's a basic `Joints` implementation in **Tres**:

```vue
<script setup lang="ts">
import { RigidBody, SphericalJoint } from '@tresjs/rapier'
import { shallowRef } from 'vue'

const bodyRefs = shallowRef(
Array.from({ length: 10 }).map(() => shallowRef(null)),
)
</script>

<template>
<RigidBody
v-for="(ref, i) in bodyRefs"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got a problem, the mental load of this example is high. I would instead, in this page, come with a very basic example, just two rigid Bodies joined (just that). The idea here is that they learn how to use it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example reflects the one in the playground (the JointsDemo example)...
Is it okay to simplify this one in this documentation but keep the demo as it is?

:key="i"
:ref="ref"
:type="i === 0 ? 'kinematic' : 'dynamic'"
:position="[i * 1.5, 0, 0]"
collider="ball"
>
<TresMesh>
<TresSphereGeometry />
<TresMeshNormalMaterial />
</TresMesh>
</RigidBody>

<SphericalJoint
v-for="(ref, i) in bodyRefs"
:key="i"
:bodies="[ref.value?.[0]?.instance, bodyRefs[i - 1]?.value?.[0]?.instance]"
:params="[
[-1.1, 0, 0],
[1.1, 0, 0],
]"
/>
</template>
```

> Preview
<img width="843" alt="Screenshot 2024-12-09 at 4 01 58PM" src="https://github.com/user-attachments/assets/9baaea58-f996-45d5-b4f4-34dae30d44cb">

### Explanation

In the above example, we created a list of 10 `RigidBody` references and mapped them using the `v-for` directive. Then, we implemented the `SphericalJoint` component, creating a Joint between each mapped `RigidBody`.

:::info
To understand how each Joint type works, please take a look at the official [Rapier Joint Documentation](https://rapier.rs/docs/user_guides/javascript/joints).
:::
74 changes: 74 additions & 0 deletions playground/src/pages/basics/JointsDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<script setup lang="ts">
import { OrbitControls } from '@tresjs/cientos'
import { TresCanvas } from '@tresjs/core'
// eslint-disable-next-line ts/ban-ts-comment
// @ts-ignore
import { type ExposedRigidBody, Physics, RigidBody, SphericalJoint } from '@tresjs/rapier'
import { ACESFilmicToneMapping, Quaternion, SRGBColorSpace } from 'three'
import { shallowRef } from 'vue'
import type { ShallowRef } from 'vue'

const gl = {
clearColor: '#82DBC5',
shadows: true,
alpha: false,
outputColorSpace: SRGBColorSpace,
toneMapping: ACESFilmicToneMapping,
}

const yRotation = shallowRef(0)
const bodyRefs: ShallowRef<ShallowRef[]> = shallowRef(
Array.from({ length: 10 }).map(() => shallowRef<ExposedRigidBody>(null)),
)

setInterval(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why setInterval?

And if you decided to use setInterval, please remember to clear the interval in the onUnmouted

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, you're right,

setInterval was used to update the rotation of the first and have that whip effect

const body = bodyRefs.value[0].value?.[0]?.instance
if (!body) { return }

yRotation.value = yRotation.value + 1

body.setNextKinematicRotation(new Quaternion(0, Math.sin(yRotation.value) * 1.5, 0, 1))
}, 1000)
</script>

<template>
<TresCanvas v-bind="gl" window-size>
<TresPerspectiveCamera :position="[0, 0, 30]" :look-at="[0, 0, 0]" />
<OrbitControls />

<Suspense>
<Physics debug>
<RigidBody
v-for="(ref, i) in bodyRefs"
:key="i"
:ref="ref"
:type="i === 0 ? 'kinematic' : 'dynamic'"
:position="[i * 1.5, 0, 0]"
collider="ball"
>
<TresMesh>
<TresSphereGeometry />
<TresMeshNormalMaterial />
</TresMesh>
</RigidBody>

<SphericalJoint
v-for="(ref, i) in bodyRefs"
:key="i"
:bodies="[ref.value?.[0]?.instance, bodyRefs[i - 1]?.value?.[0]?.instance]"
:params="[
[-1.1, 0, 0],
[1.1, 0, 0],
]"
/>

<RigidBody type="fixed">
<TresMesh :position="[0, -10, 0]">
<TresPlaneGeometry :args="[40, 40, 20]" :rotate-x="-Math.PI / 2" />
<TresMeshBasicMaterial color="#f4f4f4" />
</TresMesh>
</RigidBody>
</Physics>
</Suspense>
</TresCanvas>
</template>
5 changes: 5 additions & 0 deletions playground/src/router/routes/basics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,9 @@ export const basicsRoutes = [
name: 'Sensor',
component: () => import('../../pages/basics/SensorDemo.vue'),
},
{
path: '/basics/joints',
name: 'Joints',
component: () => import('../../pages/basics/JointsDemo.vue'),
},
]
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './colliders'
export { default as InstancedRigidBody } from './InstancedRigidBody.vue'
export * from './joints'
export { default as Physics } from './Physics.vue'
export { default as RigidBody } from './RigidBody.vue'
185 changes: 185 additions & 0 deletions src/components/joints/BaseJoint.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<script lang="ts" setup>
import { type ImpulseJoint, type JointAxesMask, type JointData, Quaternion, Vector3 } from '@dimforge/rapier3d-compat'
import { onUnmounted, shallowRef, watch } from 'vue'

import { useRapier } from '../../composables'
import type { JointProps, QuaternionArray, VectorArray } from '../../types'

const {
type = 'fixed',
bodies,
params = [
[0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0],
[0, 0, 0, 0],
],
wakeUpOnChanges = true,
} = defineProps<JointProps>()
const { world, rapier } = useRapier()

const joins = shallowRef<ImpulseJoint>()

watch(() => bodies, (bodies) => {
if (
joins.value
|| !(bodies?.[0] instanceof rapier.RigidBody)
|| !(bodies?.[1] instanceof rapier.RigidBody)
|| !Array.isArray(params)
) {
return
}

let jointParams: JointData | undefined
let hasParamsError = false

if (
type === 'fixed'
&& params.length >= 4
&& (Array.isArray(params[0]) && params[0].length >= 3)
&& (Array.isArray(params[1]) && params[1].length >= 4)
&& (Array.isArray(params[2]) && params[2].length >= 3)
&& (Array.isArray(params[3]) && params[3].length >= 4)
) {
jointParams = rapier.JointData.fixed(
new Vector3(...params[0] as VectorArray),
new Quaternion(...params[1] as QuaternionArray),
new Vector3(...params[2] as VectorArray),
new Quaternion(...params[3] as QuaternionArray),
)
}
else if (type === 'fixed') {
hasParamsError = true
}

if (
type === 'generic'
&& params.length >= 3
&& (Array.isArray(params[0]) && params[0].length >= 3)
&& (Array.isArray(params[1]) && params[1].length >= 3)
&& (Array.isArray(params[2]) && params[2].length >= 3)
&& typeof params[3] === 'number'
) {
jointParams = rapier.JointData.generic(
new Vector3(...params[0] as VectorArray),
new Vector3(...params[1] as VectorArray),
new Vector3(...params[2] as VectorArray),
params[3] as JointAxesMask,
)
}
else if (type === 'generic') {
hasParamsError = true
}

if (
type === 'prismatic'
&& params.length >= 4
&& (Array.isArray(params[0]) && params[0].length >= 3)
&& (Array.isArray(params[1]) && params[1].length >= 3)
&& (Array.isArray(params[2]) && params[2].length >= 3)
) {
jointParams = rapier.JointData.prismatic(
new Vector3(...params[0] as VectorArray),
new Vector3(...params[1] as VectorArray),
new Vector3(...params[1] as VectorArray),
)
}
else if (type === 'prismatic') {
hasParamsError = true
}

if (
type === 'revolute'
&& params.length >= 3
&& (Array.isArray(params[0]) && params[0].length >= 3)
&& (Array.isArray(params[1]) && params[1].length >= 3)
&& (Array.isArray(params[2]) && params[2].length >= 3)
) {
jointParams = rapier.JointData.revolute(
new Vector3(...params[0] as VectorArray),
new Vector3(...params[1] as VectorArray),
new Vector3(...params[2] as VectorArray),
)
}
else if (type === 'revolute') {
hasParamsError = true
}

if (
type === 'rope'
&& params.length >= 3
&& typeof params[0] === 'number'
&& (Array.isArray(params[1]) && params[1].length >= 3)
&& (Array.isArray(params[2]) && params[2].length >= 4)
) {
jointParams = rapier.JointData.rope(
params[0],
new Vector3(...params[1] as VectorArray),
new Quaternion(...params[2] as QuaternionArray),
)
}
else if (type === 'rope') {
hasParamsError = true
}

if (
type === 'spherical'
&& params.length >= 2
&& (Array.isArray(params[0]) && params[0].length >= 3)
&& (Array.isArray(params[1]) && params[1].length >= 3)
) {
jointParams = rapier.JointData.spherical(
new Vector3(...params[0] as VectorArray),
new Vector3(...params[1] as VectorArray),
)
}
else if (type === 'spherical') {
hasParamsError = true
}

if (
type === 'spring'
&& params.length >= 5
&& typeof params[0] === 'number'
&& typeof params[1] === 'number'
&& typeof params[2] === 'number'
&& (Array.isArray(params[3]) && params[3].length >= 3)
&& (Array.isArray(params[4]) && params[4].length >= 3)
) {
jointParams = rapier.JointData.spring(
params[0],
params[1],
params[2],
new Vector3(...params[3] as VectorArray),
new Vector3(...params[4] as VectorArray),
)
}
else if (type === 'spring') {
hasParamsError = true
}

if (hasParamsError) {
throw new Error(`Invalid "${type}" joint parameters`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe too difficult, but could be said what parameters have problems?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, maybe a string containing the requirement or why it the error occurs

}

if (!jointParams) {
throw new Error(`Unsupported joint type. If you think this is a bug or the "${type}" type should be implemented, please open an issue.`)
}

joins.value = world.createImpulseJoint(jointParams, bodies[0], bodies[1], wakeUpOnChanges)
})

onUnmounted(() => {
if (joins.value) {
world.removeImpulseJoint(joins.value, wakeUpOnChanges)
}
})

defineExpose({
joins,
})
</script>

<template>
<slot v-once></slot>
</template>
Loading
Loading