@solidrt/3d 0.0.54 → 0.0.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1121 -177
- package/README.md +15 -7
- package/examples/README.md +86 -5
- package/examples/aim.tsx +2 -3
- package/examples/cascades.tsx +20 -5
- package/examples/collision.tsx +250 -0
- package/examples/first-person.tsx +119 -0
- package/examples/fleet.tsx +194 -0
- package/examples/fog.tsx +173 -0
- package/examples/instanced.tsx +72 -32
- package/examples/lamps.tsx +92 -0
- package/examples/lit.tsx +3 -5
- package/examples/materials.tsx +150 -0
- package/examples/model-load.tsx +95 -0
- package/examples/model.glb +0 -0
- package/examples/model.tsx +32 -6
- package/examples/pick.tsx +2 -4
- package/examples/probe.tsx +101 -0
- package/examples/scene-background.tsx +2 -4
- package/examples/scene-basic.tsx +2 -4
- package/examples/scene-views.tsx +32 -3
- package/examples/shadows.tsx +2 -4
- package/examples/sky-lit.tsx +86 -0
- package/examples/skybox.tsx +128 -0
- package/examples/sprites.tsx +4 -6
- package/examples/standard.tsx +132 -0
- package/examples/sweep-paths.tsx +2 -4
- package/package.json +3 -3
- package/src/camera.ts +148 -0
- package/src/clip.ts +96 -0
- package/src/collision.ts +199 -0
- package/src/color.ts +32 -0
- package/src/components.tsx +773 -82
- package/src/environment-bake.ts +492 -0
- package/src/environment.ts +325 -0
- package/src/first-person.ts +306 -0
- package/src/geometry.ts +6 -2
- package/src/glsl.ts +1390 -82
- package/src/gltf.ts +432 -112
- package/src/index.ts +57 -9
- package/src/light.ts +383 -0
- package/src/material.ts +682 -216
- package/src/math.ts +110 -0
- package/src/mesh.ts +728 -0
- package/src/mixer.ts +476 -0
- package/src/model-file.ts +80 -15
- package/src/model.ts +211 -34
- package/src/node.ts +506 -0
- package/src/orbit.ts +97 -48
- package/src/scene-pointer.ts +166 -0
- package/src/scene-shadows.ts +471 -0
- package/src/scene.ts +1304 -1532
- package/tools/environment.ts +69 -0
- package/tools/model.ts +4 -3
package/src/collision.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// A kinematic character move over the scene's volume queries: Godot's
|
|
2
|
+
// CharacterBody3D.move_and_slide and Unity's CharacterController.Move as
|
|
3
|
+
// one pure function. Sweep the body along its motion, stop a skin short
|
|
4
|
+
// of the first contact, slide the rest along the contact plane, repeat a
|
|
5
|
+
// few times - with the two passes both engines wrap around that loop: a
|
|
6
|
+
// depenetration first (a body that starts inside something is pushed out
|
|
7
|
+
// before it moves) and a floor snap after (a body walking down a ramp is
|
|
8
|
+
// pulled back onto it instead of stepping into the air). Per body per
|
|
9
|
+
// frame that is the input read and this one call; the geometry runs in
|
|
10
|
+
// the spatial core.
|
|
11
|
+
//
|
|
12
|
+
// Pure on purpose: no node, no velocity state. It takes a volume where the
|
|
13
|
+
// body IS and the motion it WANTS, and returns the motion it gets plus
|
|
14
|
+
// what it touched - the first-person camera (which has no node) composes
|
|
15
|
+
// it in clampPosition, and a node-driven body applies the result with
|
|
16
|
+
// setTransform. Gravity is the caller's: fold it into `motion`; a walkable
|
|
17
|
+
// floor absorbs the vertical part (a body does not creep down a slope it
|
|
18
|
+
// can stand on), a steep one slides it.
|
|
19
|
+
|
|
20
|
+
import type { Vec3 } from "./math.ts"
|
|
21
|
+
import type { Impact, QueryOptions, Scene, Volume } from "./scene.ts"
|
|
22
|
+
|
|
23
|
+
/** Gap the body keeps from every surface, world units (Unity's skinWidth,
|
|
24
|
+
* Godot's safe_margin): what keeps float error from sinking a resting body
|
|
25
|
+
* into its floor. */
|
|
26
|
+
const SKIN = 0.01
|
|
27
|
+
/** Contacts within this angle of `up` are floor: 45 degrees, Godot's
|
|
28
|
+
* floor_max_angle and Unity's slopeLimit. */
|
|
29
|
+
const FLOOR_MAX_ANGLE = Math.PI / 4
|
|
30
|
+
/** Most contacts one call slides along before it stops where it is
|
|
31
|
+
* (Godot's max_slides). */
|
|
32
|
+
const MAX_SLIDES = 6
|
|
33
|
+
/** How far below the body a floor is pulled to while it is not rising
|
|
34
|
+
* (Godot's floor_snap_length). */
|
|
35
|
+
const FLOOR_SNAP = 0.1
|
|
36
|
+
/** Passes of the depenetration loop, one contact set pushed out per pass
|
|
37
|
+
* (Godot's recovery attempts). */
|
|
38
|
+
const MAX_RECOVERIES = 4
|
|
39
|
+
/** Motion below which a slide stops: nothing left worth sweeping. */
|
|
40
|
+
const MIN_MOVE = 1e-6
|
|
41
|
+
/** Floor of the approach cosine the skin is divided by to retreat along
|
|
42
|
+
* the motion: a grazing contact retreats a few skins, not the whole step. */
|
|
43
|
+
const SKIN_MIN_COS = 0.1
|
|
44
|
+
|
|
45
|
+
const UP: Vec3 = [0, 1, 0]
|
|
46
|
+
|
|
47
|
+
/** What moveAndSlide needs of a scene. */
|
|
48
|
+
export type MoveScene = Pick<Scene, "sweep" | "overlap">
|
|
49
|
+
|
|
50
|
+
export type MoveOptions = QueryOptions & {
|
|
51
|
+
/** The direction floors face (default [0, 1, 0]). */
|
|
52
|
+
up?: Vec3
|
|
53
|
+
/** Largest angle (radians) between a contact normal and `up` that still
|
|
54
|
+
* counts as floor (default 45 degrees). Steeper contacts are walls, and
|
|
55
|
+
* the body slides down them. */
|
|
56
|
+
floorMaxAngle?: number
|
|
57
|
+
/** Most contacts one call slides along (default 6). */
|
|
58
|
+
maxSlides?: number
|
|
59
|
+
/** Gap kept from every surface, world units (default 0.01). */
|
|
60
|
+
skin?: number
|
|
61
|
+
/** How far below the body a floor is pulled to when the motion does not
|
|
62
|
+
* rise (default 0.1; 0 disables): what keeps a walker on a ramp going
|
|
63
|
+
* down, and what decides `floor` at the end of the move - a body that
|
|
64
|
+
* ends higher than this above its floor is airborne, whatever it
|
|
65
|
+
* touched on the way. A rising motion (a jump) never snaps. */
|
|
66
|
+
floorSnap?: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type MoveResult = {
|
|
70
|
+
/** The displacement the body gets: add it to the body's position. */
|
|
71
|
+
motion: Vec3
|
|
72
|
+
/** The unit normal of the floor the body ends the move on - within
|
|
73
|
+
* `floorSnap` below it, snapped onto - else null (airborne, or on a
|
|
74
|
+
* slope too steep to stand on). With `floorSnap: 0` it is a floor met
|
|
75
|
+
* during the move instead. */
|
|
76
|
+
floor: Vec3 | null
|
|
77
|
+
/** Whether a wall (a contact steeper than a floor and flatter than a
|
|
78
|
+
* ceiling) or a ceiling was met. */
|
|
79
|
+
wall: boolean
|
|
80
|
+
ceiling: boolean
|
|
81
|
+
/** Every contact met, in order, the floor snap's last. */
|
|
82
|
+
hits: Impact[]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Move a body `motion` through the scene's colliders, sliding along what
|
|
87
|
+
* it hits (Godot's move_and_slide, Unity's CharacterController.Move): a
|
|
88
|
+
* capsule for a character, a sphere for a ball, a box for a crate. The
|
|
89
|
+
* body first pushes out of anything it starts inside, then sweeps and
|
|
90
|
+
* slides up to `maxSlides` times, then, unless the motion rises, snaps
|
|
91
|
+
* down onto a floor within `floorSnap` - the floor it reports is the one
|
|
92
|
+
* it ends on. Colliders are whatever `opts`
|
|
93
|
+
* selects (`layers`/`meshes`, as for sweep): pass the collision layer,
|
|
94
|
+
* or the body's own mesh answers for its own walls.
|
|
95
|
+
*
|
|
96
|
+
* Gravity is the caller's: fold the fall into `motion` each frame, zero
|
|
97
|
+
* it while `floor` is set (the snap keeps reporting the floor while the
|
|
98
|
+
* body stands still), and a walkable floor absorbs the vertical part -
|
|
99
|
+
* `motion` only ever moves the body along a floor it stands on, never
|
|
100
|
+
* down it.
|
|
101
|
+
*/
|
|
102
|
+
export function moveAndSlide(scene: MoveScene, volume: Volume, motion: Vec3, opts: MoveOptions = {}): MoveResult {
|
|
103
|
+
let up = opts.up ?? UP
|
|
104
|
+
let floorCos = Math.cos(opts.floorMaxAngle ?? FLOOR_MAX_ANGLE)
|
|
105
|
+
let maxSlides = opts.maxSlides ?? MAX_SLIDES
|
|
106
|
+
let skin = opts.skin ?? SKIN
|
|
107
|
+
let snap = opts.floorSnap ?? FLOOR_SNAP
|
|
108
|
+
let query: QueryOptions = { layers: opts.layers, meshes: opts.meshes }
|
|
109
|
+
let offset: Vec3 = [0, 0, 0]
|
|
110
|
+
let hits: Impact[] = []
|
|
111
|
+
let floor: Vec3 | null = null
|
|
112
|
+
let wall = false
|
|
113
|
+
let ceiling = false
|
|
114
|
+
|
|
115
|
+
// Depenetration: every contact's push-out, each only as far as the
|
|
116
|
+
// pushes so far have not already covered along its normal, so two
|
|
117
|
+
// walls of a corner do not push twice.
|
|
118
|
+
for (let pass = 0; pass < MAX_RECOVERIES; pass++) {
|
|
119
|
+
let push: Vec3 = [0, 0, 0]
|
|
120
|
+
let any = false
|
|
121
|
+
for (let c of scene.overlap(shifted(volume, offset), query)) {
|
|
122
|
+
if (c.depth <= 0) continue
|
|
123
|
+
any = true
|
|
124
|
+
let extra = c.depth + skin - dot(push, c.normal)
|
|
125
|
+
if (extra > 0) push = add(push, scale(c.normal, extra))
|
|
126
|
+
}
|
|
127
|
+
if (!any) break
|
|
128
|
+
offset = add(offset, push)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let remaining: Vec3 = [motion[0], motion[1], motion[2]]
|
|
132
|
+
for (let slide = 0; slide < maxSlides; slide++) {
|
|
133
|
+
let len = Math.hypot(remaining[0], remaining[1], remaining[2])
|
|
134
|
+
if (len < MIN_MOVE) break
|
|
135
|
+
let hit = scene.sweep(shifted(volume, offset), remaining, query)[0]
|
|
136
|
+
if (hit === undefined) {
|
|
137
|
+
offset = add(offset, remaining)
|
|
138
|
+
break
|
|
139
|
+
}
|
|
140
|
+
hits.push(hit)
|
|
141
|
+
let dir = scale(remaining, 1 / len)
|
|
142
|
+
let n = hit.normal
|
|
143
|
+
// Stop where the gap to the surface, measured along its normal, is
|
|
144
|
+
// one skin; the sweep guarantees the approach is positive.
|
|
145
|
+
let retreat = skin / Math.max(-dot(dir, n), SKIN_MIN_COS)
|
|
146
|
+
let advance = Math.max(hit.time * len - retreat, 0)
|
|
147
|
+
offset = add(offset, scale(dir, advance))
|
|
148
|
+
remaining = scale(dir, len - advance)
|
|
149
|
+
let rise = dot(n, up)
|
|
150
|
+
if (rise >= floorCos) {
|
|
151
|
+
// A floor takes the vertical part (the fall, a landing) and passes
|
|
152
|
+
// the horizontal part along its plane.
|
|
153
|
+
floor = n
|
|
154
|
+
let level = sub(remaining, scale(up, dot(remaining, up)))
|
|
155
|
+
remaining = sub(level, scale(n, dot(level, n)))
|
|
156
|
+
} else {
|
|
157
|
+
if (rise <= -floorCos) ceiling = true
|
|
158
|
+
else wall = true
|
|
159
|
+
remaining = sub(remaining, scale(n, dot(remaining, n)))
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Where the body ends decides the floor: a contact met on the way
|
|
164
|
+
// (a ramp's top before a big step past its crest) is not one to stand
|
|
165
|
+
// on unless it is still within reach below.
|
|
166
|
+
if (snap > 0 && dot(motion, up) <= 0) {
|
|
167
|
+
floor = null
|
|
168
|
+
let hit = scene.sweep(shifted(volume, offset), scale(up, -snap), query)[0]
|
|
169
|
+
if (hit !== undefined && dot(hit.normal, up) >= floorCos) {
|
|
170
|
+
hits.push(hit)
|
|
171
|
+
let retreat = skin / Math.max(dot(hit.normal, up), SKIN_MIN_COS)
|
|
172
|
+
offset = add(offset, scale(up, -Math.max(hit.time * snap - retreat, 0)))
|
|
173
|
+
floor = hit.normal
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { motion: offset, floor, wall, ceiling, hits }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The volume displaced by `d`. */
|
|
180
|
+
function shifted(volume: Volume, d: Vec3): Volume {
|
|
181
|
+
if ("center" in volume) return { ...volume, center: add(volume.center, d) }
|
|
182
|
+
return { ...volume, a: add(volume.a, d), b: add(volume.b, d) }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function add(a: Vec3, b: Vec3): Vec3 {
|
|
186
|
+
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function sub(a: Vec3, b: Vec3): Vec3 {
|
|
190
|
+
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function scale(v: Vec3, s: number): Vec3 {
|
|
194
|
+
return [v[0] * s, v[1] * s, v[2] * s]
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function dot(a: Vec3, b: Vec3): number {
|
|
198
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
|
199
|
+
}
|
package/src/color.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// The sRGB transfer functions (IEC 61966-2-1) in JS, the JS half of the
|
|
2
|
+
// package's color contract: every [r, g, b] color option - material
|
|
3
|
+
// color and emissive, light color, hemisphere sky and ground, fog color -
|
|
4
|
+
// is sRGB-encoded, what a color picker shows, and the library decodes it to
|
|
5
|
+
// linear light when it writes the uniform (Three's ColorManagement,
|
|
6
|
+
// Godot's inspector colors, Unity's linear color space all do the same).
|
|
7
|
+
// Shading then runs in linear light and the fragment's output stage
|
|
8
|
+
// (OUTPUT in ./glsl) encodes the result back. Values written straight to
|
|
9
|
+
// a uniform (scene.setParams, setMeshParams) and vertex colors are linear,
|
|
10
|
+
// as in every engine; encode or decode them here when they come from a
|
|
11
|
+
// picker.
|
|
12
|
+
|
|
13
|
+
/** Decode one sRGB-encoded component (0..1) to linear light. Above 1 the
|
|
14
|
+
* curve continues, so an intensity folded into a color survives. */
|
|
15
|
+
export function srgbToLinear(c: number): number {
|
|
16
|
+
if (c <= 0.04045) return c / 12.92
|
|
17
|
+
return Math.pow((c + 0.055) / 1.055, 2.4)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Encode one linear-light component to sRGB: the inverse of srgbToLinear. */
|
|
21
|
+
export function linearToSrgb(c: number): number {
|
|
22
|
+
if (c <= 0.0031308) return c * 12.92
|
|
23
|
+
return 1.055 * Math.pow(c, 1 / 2.4) - 0.055
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A copy of an [r, g, b] or [r, g, b, a] color with its rgb decoded to
|
|
27
|
+
* linear light; alpha is coverage, never encoded, and passes through. */
|
|
28
|
+
export function linearColor(color: readonly number[]): number[] {
|
|
29
|
+
let out = color.slice()
|
|
30
|
+
for (let i = 0; i < 3 && i < out.length; i++) out[i] = srgbToLinear(out[i]!)
|
|
31
|
+
return out
|
|
32
|
+
}
|