Skip to content

Commit

Permalink
add roll ball, attractor demos
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexWarnes committed Oct 14, 2022
1 parent ce8d02d commit 3be368f
Show file tree
Hide file tree
Showing 19 changed files with 834 additions and 0 deletions.
66 changes: 66 additions & 0 deletions src/lib/components/Attractor.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<script lang="ts">
import type { RigidBody } from '@dimforge/rapier3d-compat'
import { Mesh, useFrame, Object3DInstance, type Position } from '@threlte/core'
import { SphereBufferGeometry, MeshBasicMaterial, Vector3, Object3D } from 'three'
import { useRapier } from '@threlte/rapier'
export let position: Position = {};
export let strength: number = 1
export let range: number = 50
export let showHelper: boolean = false
export let gravityType: 'static' | 'linear' | 'newtonian' = 'static'
export let gravitationalConstant: number = 6.673e-11
const { world } = useRapier()
const gravitySource = new Vector3()
let obj = new Object3D()
const calcForceByType = {
static: (s: number, m2: number, r: number, d: number, G: number): number => s,
linear: (s: number, m2: number, r: number, d: number, G: number) => s * (d / r),
newtonian: (s: number, m2: number, r: number, d: number, G: number) =>
(G * s * m2) / Math.pow(d, 2)
}
function applyImpulseToBodiesInRange() {
const impulseVector = new Vector3()
obj.getWorldPosition(gravitySource)
world.forEachRigidBody((body: RigidBody) => {
const { x, y, z } = body.translation()
const bodyV3: Vector3 = new Vector3(x, y, z)
const distance: number = gravitySource.distanceTo(bodyV3)
if (distance < range) {
let force = calcForceByType[gravityType](
strength,
body.mass(),
range,
distance,
gravitationalConstant
)
// Prevent wild forces when Attractors collide
force = force === Infinity ? strength : force
impulseVector.subVectors(gravitySource, bodyV3).normalize().multiplyScalar(force)
body.applyImpulse(impulseVector, true)
}
})
}
useFrame(() => {
applyImpulseToBodiesInRange()
})
</script>

<Object3DInstance bind:object={obj} {position} />

{#if showHelper}
<Mesh
geometry={new SphereBufferGeometry(range)}
material={new MeshBasicMaterial({ wireframe: true })}
{position}
/>
<Mesh
geometry={new SphereBufferGeometry()}
material={new MeshBasicMaterial({ color: 'black', wireframe: false })}
{position}
/>
{/if}
16 changes: 16 additions & 0 deletions src/routes/showcase/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@
import Paper from '$lib/components/Paper.svelte';
const scenes = [
{
name: 'Roll the Ball',
path: '/showcase/roll-the-ball',
thumb: './assets/roll-the-ball-thumb.png',
description:
'Roll the ball with arrows/wasd and jump with spacebar! Try to get the ball in the basket at the end of the track.',
tools: ['threlte', 'rapier',]
},
{
name: 'Attractors',
path: '/showcase/attractors',
thumb: './assets/attractors-thumb.png',
description:
'Initial experiments with an attractor that simulates a source of gravity. Any rigid-body within range will be "pulled" toward the attractor.',
tools: ['threlte', 'rapier',]
},
{
name: 'Cannon Fire',
path: '/showcase/cannon-fire',
Expand Down
38 changes: 38 additions & 0 deletions src/routes/showcase/attractors/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script>
import { Canvas } from '@threlte/core';
import { World, } from '@threlte/rapier';
import { dynamicAttractors, } from './_routeLib/systemState';
import Scene from './_routeLib/Scene.svelte';
</script>

<div class="wrapper">
<Canvas>
<World gravity={{y: 0}}>
<Scene />
</World>
</Canvas>
<div class="controls">
<label>
Dynamic Attractors
<input
type="checkbox"
bind:checked={$dynamicAttractors} />
</label>
</div>
</div>

<style>
.wrapper {
position: fixed;
width: 100%;
height: 100%;
background: black;
}
.controls {
position: absolute;
top: 1rem;
left: 1rem;
background: #00000088;
color: #fafbfc;
}
</style>
103 changes: 103 additions & 0 deletions src/routes/showcase/attractors/_routeLib/Objects.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<script lang="ts">
import { MeshStandardMaterial, Vector3, SphereGeometry, MeshBasicMaterial } from 'three';
import { Collider, RigidBody } from '@threlte/rapier';
import { Mesh } from '@threlte/core';
import { nbodyCount, dynamicAttractors } from './systemState';
import Attractor from '$lib/components/Attractor.svelte';
import { randomVec3 } from '$lib';
$: attractors = [
{
id: 'LEFT',
pos: { x: -100 },
m: 15,
range: 75
},
{
id: 'CENTER',
pos: { x: 0, y: 50 },
m: 15,
range: 75
},
{
id: 'RIGHT',
pos: { x: 100 },
m: 15,
range: 75
}
];
$: randomBodies = Array($nbodyCount)
.fill('x')
.map((_) => {
return {
id: Date.now() + Math.random(),
pos: randomVec3({ x: [-75, 75], y: [25, 50], z: [-40, 40] }),
lv: randomVec3({ x: [-2, 2], y: [0, -5], z: [-2, 2] })
};
});
</script>

{#each attractors as body (body.id)}
{#if $dynamicAttractors}
<RigidBody position={body.pos}>
<Collider shape="ball" args={[0.5]} mass={body.m * 2} />
<Mesh
geometry={new SphereGeometry()}
scale={0.5}
material={new MeshBasicMaterial({ color: 'gold' })}
/>
<Mesh
geometry={new SphereGeometry()}
scale={body.range * 1.5}
material={new MeshBasicMaterial({
wireframe: true,
transparent: true,
opacity: 0.125,
color: 'cyan'
})}
/>

<Attractor
strength={body.m}
gravityType="linear"
gravitationalConstant={0.26674}
range={body.range * 1.5}
/>
</RigidBody>
{:else}
<Mesh
position={body.pos}
geometry={new SphereGeometry(0.5)}
material={new MeshBasicMaterial({ color: 'gold' })}
>
<Mesh
geometry={new SphereGeometry()}
scale={body.range}
material={new MeshBasicMaterial({
wireframe: true,
transparent: true,
opacity: 0.125,
color: 'cyan'
})}
/>

<Attractor
strength={body.m}
gravityType="linear"
gravitationalConstant={0.26674}
range={body.range}
/>
</Mesh>
{/if}
{/each}

{#each randomBodies as body}
<RigidBody position={body.pos.multiplyScalar(3)} linearVelocity={body.lv}>
<Collider shape="ball" args={[1]} mass={1} />
<Mesh
geometry={new SphereGeometry()}
scale={1}
material={new MeshBasicMaterial({ color: 'red' })}
/>
</RigidBody>
{/each}
18 changes: 18 additions & 0 deletions src/routes/showcase/attractors/_routeLib/Scene.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script>
import {
PerspectiveCamera,
DirectionalLight,
AmbientLight,
OrbitControls,
} from '@threlte/core';
import Objects from "./Objects.svelte";
</script>

<PerspectiveCamera position={{ x: 0, y: 0, z: 400, }} fov={65} far="{1000}">
<OrbitControls />
</PerspectiveCamera>

<DirectionalLight position={{x: 10, y: 10, z: 0}}/>
<AmbientLight intensity={0.35} />

<Objects />
5 changes: 5 additions & 0 deletions src/routes/showcase/attractors/_routeLib/systemState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { writable } from "svelte/store";

export const nbodyCount = writable(100);
export const attractorCount = writable(3);
export const dynamicAttractors = writable(false);
86 changes: 86 additions & 0 deletions src/routes/showcase/roll-the-ball/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<script>
// My attempt to recreate the game from Bruno Simon's tweet: https://twitter.com/bruno_simon/status/1572301729894666240
import { Canvas } from '@threlte/core';
import { World, Debug } from '@threlte/rapier';
import { status, time } from './_routeLib/state';
import { slide } from "svelte/transition";
import Scene from './_routeLib/Scene.svelte';
import Timer from './_routeLib/Timer.svelte';
</script>

<Timer />
<div class="wrapper">
<Canvas>
<World>
<!-- <Debug /> -->
<Scene />
</World>
</Canvas>
<div class="timer row">
<span>
{$time}
</span>
</div>
<div class="action row">
{#if $status === "IDLE"}
<button on:click={() => $status = "PLAYING"} transition:slide>
PLAY
</button>
{:else if $status === "DONE"}
<button on:click={() => $status = "IDLE"} in:slide>
RESET
</button>
{/if}
</div>
</div>

<style>
* {
font-family: Verdana;
}
.wrapper {
position: fixed;
width: 100%;
height: 100%;
background-color: cornsilk;
}
.row {
position: absolute;
left: 0;
width: 100%;
}
.timer.row {
top: 10%;
width: 100%;
margin: 0;
background: #00000055;
color: white;
padding: 1rem 0;
text-align: center;
font-variant-numeric: tabular-nums;
font-weight: 600;
}
.action.row {
top: 40%;
}
.action.row button {
width: 100%;
height: 100%;
margin: 0;
background: #00000055;
color: white;
padding: 1rem 0;
cursor: pointer;
font-weight: 600;
border: 2px solid transparent;
transition: all 200ms ease;
}
.action.row button:hover {
border: 2px solid white;
background: #000000a1;
}
</style>
42 changes: 42 additions & 0 deletions src/routes/showcase/roll-the-ball/_routeLib/CameraRig.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<script>
import { PerspectiveCamera, OrbitControls } from "@threlte/core";
import { tweened } from "svelte/motion";
import { quintOut } from "svelte/easing";
import { selectRigidBodyPos, rbp } from "./state";
const initialCameraPosition = {
x: 0,
y: 10,
z: 15,
}
const x = selectRigidBodyPos('x');
const y = selectRigidBodyPos('y');
const z = selectRigidBodyPos('z');
const camX = tweened($x, { duration: 2000, easing: quintOut });
const camY = tweened($y, { duration: 2000, easing: quintOut });
const camZ = tweened($z, { duration: 1000, easing: quintOut });
$: if (isNaN($x)){
camX.set(initialCameraPosition.x, { duration: 4000 });
} else {
camX.set($x);
}
$: if (isNaN($y)){
camY.set(initialCameraPosition.y, { duration: 4000 });
} else {
camY.set($y + 5);
}
$: if (isNaN($z)){
camZ.set(initialCameraPosition.z, { duration: 4000 });
} else {
camZ.set($z + 20);
}
</script>

<PerspectiveCamera
position={{x: $camX, y: $camY, z: $camZ}}
lookAt={{x: $camX, y: $camY - 5, z: $camZ - 20}}
fov={55}
/>
Loading

0 comments on commit 3be368f

Please sign in to comment.