lecodes-cli 0.1.6 → 0.2.0
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/README.md +7 -1
- package/dist/index.js +202 -84
- package/package.json +4 -4
- package/runtime/sdk/animate/animate.ts +238 -0
- package/runtime/sdk/animate/bezier.ts +138 -0
- package/runtime/sdk/animate/easings.ts +126 -0
- package/runtime/sdk/core/Aspect.ts +151 -0
- package/runtime/sdk/core/color.ts +66 -0
- package/runtime/sdk/core/events.ts +38 -0
- package/runtime/sdk/core/registry.ts +23 -0
- package/runtime/sdk/g2/Camera2D.ts +33 -0
- package/runtime/sdk/g2/CharacterController2D.ts +75 -0
- package/runtime/sdk/g2/Node2D.ts +194 -0
- package/runtime/sdk/g2/Physics2D.ts +126 -0
- package/runtime/sdk/g2/Scene2D.ts +101 -0
- package/runtime/sdk/g2/Shape.ts +63 -0
- package/runtime/sdk/g2/Sprite.ts +79 -0
- package/runtime/sdk/g2/SpriteAnimation.ts +90 -0
- package/runtime/sdk/g2/Texture2D.ts +37 -0
- package/runtime/sdk/g2/Tilemap.ts +41 -0
- package/runtime/sdk/g2/Trigger.ts +47 -0
- package/runtime/sdk/g2/loop.ts +39 -0
- package/runtime/sdk/g2/touch.ts +83 -0
- package/runtime/sdk/gl/Camera.ts +40 -0
- package/runtime/sdk/gl/Collider.ts +29 -0
- package/runtime/sdk/gl/Geometry.ts +218 -0
- package/runtime/sdk/gl/Light.ts +37 -0
- package/runtime/sdk/gl/Material.ts +124 -0
- package/runtime/sdk/gl/Mesh.ts +100 -0
- package/runtime/sdk/gl/Model.ts +40 -0
- package/runtime/sdk/gl/ModelAnimation.ts +64 -0
- package/runtime/sdk/gl/Node.ts +234 -0
- package/runtime/sdk/gl/Noise.ts +30 -0
- package/runtime/sdk/gl/Particles.ts +185 -0
- package/runtime/sdk/gl/Physics.ts +95 -0
- package/runtime/sdk/gl/Plane.ts +53 -0
- package/runtime/sdk/gl/Ray.ts +16 -0
- package/runtime/sdk/gl/Scene.ts +179 -0
- package/runtime/sdk/gl/Texture.ts +35 -0
- package/runtime/sdk/gl/controls.ts +95 -0
- package/runtime/sdk/gl/state.ts +6 -0
- package/runtime/sdk/gl/touch.ts +56 -0
- package/runtime/sdk/inject.ts +85 -0
- package/runtime/sdk/math/Mathf.ts +118 -0
- package/runtime/sdk/math/mat4.ts +278 -0
- package/runtime/sdk/math/quat.ts +232 -0
- package/runtime/sdk/math/vec.ts +255 -0
- package/runtime/sdk/plugins/qr.ts +36 -0
- package/runtime/sdk/runtime/device.ts +33 -0
- package/runtime/sdk/runtime/fetch.ts +71 -0
- package/runtime/sdk/runtime/files.ts +23 -0
- package/runtime/sdk/runtime/input.ts +12 -0
- package/runtime/sdk/runtime/media.ts +111 -0
- package/runtime/sdk/runtime/misc.ts +12 -0
- package/runtime/sdk/runtime/net.ts +36 -0
- package/runtime/sdk/runtime/storage.ts +13 -0
- package/runtime/sdk/runtime/touch.ts +50 -0
- package/runtime/sdk/ui/UI.ts +19 -0
- package/runtime/sdk/ui/UIButton.ts +84 -0
- package/runtime/sdk/ui/UIContainer.ts +100 -0
- package/runtime/sdk/ui/UIImage.ts +68 -0
- package/runtime/sdk/ui/UIInput.ts +78 -0
- package/runtime/sdk/ui/UINode.ts +342 -0
- package/runtime/sdk/ui/UIScreen.ts +122 -0
- package/runtime/sdk/ui/UIScrollable.ts +70 -0
- package/runtime/sdk/ui/UISpacer.ts +14 -0
- package/runtime/sdk/ui/UIText.ts +40 -0
- package/runtime/sdk/ui/UIVideo.ts +38 -0
- package/runtime/sdk/ui/UIVirtualizedList.ts +222 -0
- package/runtime/sdk/ui/UIWidget.ts +97 -0
- package/runtime/sdk/ui/fonts.ts +10 -0
- package/runtime/sdk/ui/router.ts +73 -0
- package/runtime/inject.js +0 -4629
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// Mesh geometry: vertex / normal / index / uv buffers, plus the primitive builders. `Geometry.box()`
|
|
2
|
+
// etc. return a Geometry you can hand to Mesh.from(); Mesh.box() is sugar over them. Ported from
|
|
3
|
+
// worker/src/primitives/*.
|
|
4
|
+
|
|
5
|
+
import { cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
6
|
+
|
|
7
|
+
export type MeshKind = "triangles" | "vertices" | "edges"
|
|
8
|
+
|
|
9
|
+
// local tuple helpers — geometry building works on raw Float32Array buffers, so it stays out of the
|
|
10
|
+
// Vec3 wrapper (no per-vertex allocation).
|
|
11
|
+
type Tup3 = [number, number, number]
|
|
12
|
+
const cross = (a: Tup3, b: Tup3): Tup3 => [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] ]
|
|
13
|
+
const normalize = (a: Tup3): Tup3 => {
|
|
14
|
+
const l = Math.hypot(a[0], a[1], a[2])
|
|
15
|
+
return l === 0 ? [ 0, 0, 0 ] : [ a[0] / l, a[1] / l, a[2] / l ]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class Geometry {
|
|
19
|
+
vertices: Float32Array
|
|
20
|
+
normals: Float32Array
|
|
21
|
+
indices: Uint16Array
|
|
22
|
+
uv: Float32Array
|
|
23
|
+
/** @internal native mesh-type enum (0 triangles / 1 edges / 2 vertices). */
|
|
24
|
+
_meshKind = 0
|
|
25
|
+
|
|
26
|
+
constructor(vertices: Float32Array, normals: Float32Array, indices: Uint16Array, uv: Float32Array) {
|
|
27
|
+
this.vertices = vertices
|
|
28
|
+
this.normals = normals
|
|
29
|
+
this.indices = indices
|
|
30
|
+
this.uv = uv
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
translate(x: number, y: number, z: number): this {
|
|
34
|
+
for (let i = 0; i < this.vertices.length; i += 3) {
|
|
35
|
+
this.vertices[i] += x; this.vertices[i + 1] += y; this.vertices[i + 2] += z
|
|
36
|
+
}
|
|
37
|
+
return this
|
|
38
|
+
}
|
|
39
|
+
scale(x: number, y: number, z: number): this {
|
|
40
|
+
for (let i = 0; i < this.vertices.length; i += 3) {
|
|
41
|
+
this.vertices[i] *= x; this.vertices[i + 1] *= y; this.vertices[i + 2] *= z
|
|
42
|
+
}
|
|
43
|
+
return this
|
|
44
|
+
}
|
|
45
|
+
scaleUV(x: number, y: number): this {
|
|
46
|
+
for (let i = 0; i < this.uv.length; i += 2) { this.uv[i] *= x; this.uv[i + 1] *= y }
|
|
47
|
+
return this
|
|
48
|
+
}
|
|
49
|
+
set kind(kind: MeshKind) {
|
|
50
|
+
this._meshKind = kind === "edges" ? 1 : kind === "vertices" ? 2 : 0
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static box(): Geometry { return createCube() }
|
|
54
|
+
static sphere(options?: SphereOptions): Geometry { return createSphere(options) }
|
|
55
|
+
static cylinder(options?: CylinderOptions): Geometry { return createCylinder(options) }
|
|
56
|
+
static plane(options?: PlaneOptions): Geometry { return createPlane(options) }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- cube ---
|
|
60
|
+
const createCube = (): Geometry => {
|
|
61
|
+
const vertices = new Float32Array(6 * 4 * 3)
|
|
62
|
+
const normals = new Float32Array(6 * 4 * 3)
|
|
63
|
+
const uv = new Float32Array(6 * 4 * 3)
|
|
64
|
+
const indices = new Uint16Array(6 * 6)
|
|
65
|
+
let cnt = 0, indicesCnt = 0
|
|
66
|
+
const sides = [ -0.5, 0.5 ]
|
|
67
|
+
for (let i = 0; i < 6; i++) {
|
|
68
|
+
const s = cnt
|
|
69
|
+
const side = i % 2 === 0 ? -0.5 : 0.5
|
|
70
|
+
for (const a of sides) {
|
|
71
|
+
for (const b of sides) {
|
|
72
|
+
uv.set([ a + 0.5, b + 0.5 ], cnt * 2)
|
|
73
|
+
if (i === 0 || i === 1) { vertices.set([ side, b, -a ], cnt * 3); normals.set([ side * 2, 0, 0 ], cnt * 3) }
|
|
74
|
+
if (i === 2 || i === 3) { vertices.set([ a, side, -b ], cnt * 3); normals.set([ 0, side * 2, 0 ], cnt * 3) }
|
|
75
|
+
if (i === 4 || i === 5) { vertices.set([ a, b, side ], cnt * 3); normals.set([ 0, 0, side * 2 ], cnt * 3) }
|
|
76
|
+
cnt++
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const s1 = s + 1, s2 = s + 2, s3 = s + 3
|
|
80
|
+
if (i % 2 === 0) indices.set([ s, s1, s2, s3, s2, s1 ], indicesCnt)
|
|
81
|
+
else indices.set([ s2, s1, s, s1, s2, s3 ], indicesCnt)
|
|
82
|
+
indicesCnt += 6
|
|
83
|
+
}
|
|
84
|
+
return new Geometry(vertices, normals, indices, uv)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --- sphere ---
|
|
88
|
+
export type SphereOptions = { radius?: number, widthSegments?: number, heightSegments?: number }
|
|
89
|
+
const createSphere = (options: SphereOptions = {}): Geometry => {
|
|
90
|
+
const { radius = 0.5, widthSegments = 32, heightSegments = 32 } = options
|
|
91
|
+
const verticesCount = (widthSegments + 1) * (heightSegments - 1) + widthSegments * 2
|
|
92
|
+
const vertices = new Float32Array(verticesCount * 3)
|
|
93
|
+
const normals = new Float32Array(verticesCount * 3)
|
|
94
|
+
const uvs = new Float32Array(verticesCount * 2)
|
|
95
|
+
const indices = new Uint16Array(widthSegments * (heightSegments - 2) * 6 + widthSegments * 3 * 2)
|
|
96
|
+
const phiLength = Math.PI * 2, thetaLength = Math.PI
|
|
97
|
+
let vertexCnt = 0, indicesCnt = 0
|
|
98
|
+
for (let iy = 1; iy < heightSegments; iy++) {
|
|
99
|
+
const v = iy / heightSegments
|
|
100
|
+
for (let ix = 0; ix <= widthSegments; ix++) {
|
|
101
|
+
const u = ix / widthSegments
|
|
102
|
+
const point: [number, number, number] = [
|
|
103
|
+
-Math.cos(u * phiLength) * Math.sin(v * thetaLength),
|
|
104
|
+
Math.cos(v * thetaLength),
|
|
105
|
+
Math.sin(u * phiLength) * Math.sin(v * thetaLength),
|
|
106
|
+
]
|
|
107
|
+
vertices.set([ point[0] * radius, point[1] * radius, point[2] * radius ], vertexCnt * 3)
|
|
108
|
+
normals.set(point, vertexCnt * 3)
|
|
109
|
+
uvs.set([ u, 1 - v ], vertexCnt * 2)
|
|
110
|
+
if (iy > 1 && ix > 0) {
|
|
111
|
+
const s = vertexCnt - 1
|
|
112
|
+
const s1 = vertexCnt - (widthSegments + 1) - 1
|
|
113
|
+
indices.set([ s + 1, s1, s, s1, s + 1, s1 + 1 ], indicesCnt)
|
|
114
|
+
indicesCnt += 6
|
|
115
|
+
}
|
|
116
|
+
vertexCnt++
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const sides = [ -radius, radius ]
|
|
120
|
+
const circleStart2 = vertexCnt - (widthSegments + 1)
|
|
121
|
+
for (const side of sides) {
|
|
122
|
+
const circleStart = side > 0 ? 0 : circleStart2
|
|
123
|
+
for (let ix = 0; ix < widthSegments; ix++) {
|
|
124
|
+
const u = (ix + 0.5) / widthSegments
|
|
125
|
+
vertices.set([ 0, side, 0 ], vertexCnt * 3)
|
|
126
|
+
normals.set([ 0, side < 0 ? -1 : 1, 0 ], vertexCnt * 3)
|
|
127
|
+
uvs.set([ u, side < 0 ? 0 : 1 ], vertexCnt * 2)
|
|
128
|
+
const s = circleStart + ix
|
|
129
|
+
if (side > 0) indices.set([ s, s + 1, vertexCnt ], indicesCnt)
|
|
130
|
+
else indices.set([ vertexCnt, s + 1, s ], indicesCnt)
|
|
131
|
+
indicesCnt += 3
|
|
132
|
+
vertexCnt++
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return new Geometry(vertices, normals, indices, uvs)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --- cylinder ---
|
|
139
|
+
export type CylinderOptions = { radius?: number, radiusTop?: number, radiusBottom?: number, edges?: number, smooth?: boolean }
|
|
140
|
+
const createCylinder = (options: CylinderOptions = {}): Geometry => {
|
|
141
|
+
const { edges = 32, smooth = true, radius = 0.5, radiusTop = radius, radiusBottom = radius } = options
|
|
142
|
+
const verticesCount = edges * 4 + (edges + 1) * 2
|
|
143
|
+
const vertices = new Float32Array(verticesCount * 3)
|
|
144
|
+
const normals = new Float32Array(verticesCount * 3)
|
|
145
|
+
const uv = new Float32Array(verticesCount * 2)
|
|
146
|
+
const indices = new Uint16Array(edges * 6 + edges * 6)
|
|
147
|
+
const sides = [ -0.5, 0.5 ]
|
|
148
|
+
let cnt = 0, indicesCnt = 0
|
|
149
|
+
for (let i = 0; i < edges; i++) {
|
|
150
|
+
const s = cnt
|
|
151
|
+
const normalX = Math.cos((i + 0.5) / edges * Math.PI * 2)
|
|
152
|
+
const normalY = Math.sin((i + 0.5) / edges * Math.PI * 2)
|
|
153
|
+
for (let j = 0; j < 2; j++) {
|
|
154
|
+
const x = Math.cos((i + j) / edges * Math.PI * 2)
|
|
155
|
+
const y = Math.sin((i + j) / edges * Math.PI * 2)
|
|
156
|
+
for (const side of sides) {
|
|
157
|
+
if (side > 0) vertices.set([ x * radiusTop, side, y * radiusTop ], cnt * 3)
|
|
158
|
+
else vertices.set([ x * radiusBottom, side, y * radiusBottom ], cnt * 3)
|
|
159
|
+
if (smooth) normals.set([ x, 0, y ], cnt * 3)
|
|
160
|
+
else normals.set([ normalX, 0, normalY ], cnt * 3)
|
|
161
|
+
uv.set([ (i + j) / edges, side + 0.5 ], cnt * 2)
|
|
162
|
+
cnt++
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const s1 = s + 1, s2 = (s + 2) % (edges * 4), s3 = (s + 3) % (edges * 4)
|
|
166
|
+
indices.set([ s, s1, s2, s3, s2, s1 ], indicesCnt)
|
|
167
|
+
indicesCnt += 6
|
|
168
|
+
}
|
|
169
|
+
for (const side of sides) {
|
|
170
|
+
const center = cnt
|
|
171
|
+
vertices.set([ 0, side, 0 ], cnt * 3); normals.set([ 0, side * 2, 0 ], cnt * 3); cnt++
|
|
172
|
+
for (let i = 0; i < edges; i++) {
|
|
173
|
+
const x = Math.cos(i / edges * Math.PI * 2), y = Math.sin(i / edges * Math.PI * 2)
|
|
174
|
+
if (side > 0) vertices.set([ x * radiusTop, side, y * radiusTop ], cnt * 3)
|
|
175
|
+
else vertices.set([ x * radiusBottom, side, y * radiusBottom ], cnt * 3)
|
|
176
|
+
normals.set([ 0, side * 2, 0 ], cnt * 3)
|
|
177
|
+
if (side < 0) indices.set([ center, center + i + 1, center + 1 + ((i + 1) % edges) ], indicesCnt)
|
|
178
|
+
else indices.set([ center + 1 + ((i + 1) % edges), center + i + 1, center ], indicesCnt)
|
|
179
|
+
indicesCnt += 3
|
|
180
|
+
cnt++
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return new Geometry(vertices, normals, indices, uv)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// --- plane ---
|
|
187
|
+
export type PlaneOptions = { normal?: Vec3Like }
|
|
188
|
+
const createPlane = (options: PlaneOptions = {}): Geometry => {
|
|
189
|
+
const normal: Tup3 = options.normal ? [ cx(options.normal), cy(options.normal), cz(options.normal) ] : [ 0, 0, 1 ]
|
|
190
|
+
const vertices = new Float32Array(4 * 3)
|
|
191
|
+
const normals = new Float32Array(4 * 3)
|
|
192
|
+
const uvs = new Float32Array(4 * 2)
|
|
193
|
+
const sides = [ -0.5, 0.5 ]
|
|
194
|
+
let cnt = 0
|
|
195
|
+
let upVector: Tup3, rightVec: Tup3
|
|
196
|
+
if (normal[1] === 0 && normal[2] === 0) { upVector = [ 0, 1, 0 ]; rightVec = [ 0, 0, -normal[0] ] }
|
|
197
|
+
else if (normal[0] === 0 && normal[2] === 0) { upVector = [ 0, 0, 1 ]; rightVec = [ -normal[1], 0, 0 ] }
|
|
198
|
+
else if (normal[0] === 0 && normal[1] === 0) { upVector = [ 0, 1, 0 ]; rightVec = [ normal[2], 0, 0 ] } // тут было верно
|
|
199
|
+
else {
|
|
200
|
+
const temp: Tup3 = Math.abs(normal[0]) < 0.5 ? [ 1, 0, 0 ] : [ 0, 0, 1 ]
|
|
201
|
+
upVector = normalize(cross(temp, normal))
|
|
202
|
+
rightVec = normalize(cross(upVector, normal))
|
|
203
|
+
}
|
|
204
|
+
for (const a of sides) {
|
|
205
|
+
for (const b of sides) {
|
|
206
|
+
normals.set(normal, cnt * 3)
|
|
207
|
+
uvs.set([ a + 0.5, b + 0.5 ], cnt * 2)
|
|
208
|
+
vertices.set([
|
|
209
|
+
rightVec[0] * a + upVector[0] * b,
|
|
210
|
+
rightVec[1] * a + upVector[1] * b,
|
|
211
|
+
rightVec[2] * a + upVector[2] * b,
|
|
212
|
+
], cnt * 3)
|
|
213
|
+
cnt++
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const indices = new Uint16Array([ 2, 1, 0, 1, 2, 3 ])
|
|
217
|
+
return new Geometry(vertices, normals, indices, uvs)
|
|
218
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Lights. Currently a directional sun (the engine's primary light). Add it to a scene like any node.
|
|
2
|
+
|
|
3
|
+
import { Color, type ColorInput } from "../core/color"
|
|
4
|
+
import type { Vec3Like } from "../math/vec"
|
|
5
|
+
import { Node } from "./Node"
|
|
6
|
+
|
|
7
|
+
export type SunOptions = {
|
|
8
|
+
/** Light direction (points where the light travels). Defaults to a typical key-light angle. */
|
|
9
|
+
direction?: Vec3Like
|
|
10
|
+
/** Luminous intensity. */
|
|
11
|
+
intensity?: number
|
|
12
|
+
color?: ColorInput
|
|
13
|
+
/**
|
|
14
|
+
* Shadow quality (default 1):
|
|
15
|
+
* 0 - no shadows
|
|
16
|
+
* 1 - standard (1024 shadow map, hard PCF edges)
|
|
17
|
+
* 2 - high (2048 + stable, soft contact-hardening edges)
|
|
18
|
+
* 3 - best (4096 + stable, PCSS soft shadows)
|
|
19
|
+
* Higher values cost more GPU/memory.
|
|
20
|
+
*/
|
|
21
|
+
shadowsQuality?: 0 | 1 | 2 | 3
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class Light extends Node {
|
|
25
|
+
/** A directional sun light. */
|
|
26
|
+
static sun(options: SunOptions = {}): Light {
|
|
27
|
+
const light = new Light()
|
|
28
|
+
const [ dx, dy, dz ] = options.direction ?? [ 0.548267, -0.473983, -0.689016 ]
|
|
29
|
+
_creator.createSunLight(
|
|
30
|
+
light.id, dx, dy, dz,
|
|
31
|
+
options.intensity ?? 100000,
|
|
32
|
+
Color.toPackedRgb(options.color ?? 0xffffff),
|
|
33
|
+
options.shadowsQuality ?? 1,
|
|
34
|
+
)
|
|
35
|
+
return light
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// A material instance over creator-gl. Uniforms are set through a Proxy that lowers each JS value to
|
|
2
|
+
// the right native uniform call. The common materials are static factories (Material.lit/unlit/
|
|
3
|
+
// video/shadow); Material.load brings in a custom compiled shader. Ported from worker/src/Material.ts,
|
|
4
|
+
// with the Lit/Unlit/... subclasses collapsed into factories and color accepting any ColorInput.
|
|
5
|
+
|
|
6
|
+
import { Color, type ColorInput } from "../core/color"
|
|
7
|
+
import { fetch, type FetchResponse } from "../runtime/fetch"
|
|
8
|
+
import { Texture } from "./Texture"
|
|
9
|
+
|
|
10
|
+
type UniformValue = string | Texture | number | boolean | number[] | Float32Array | null
|
|
11
|
+
|
|
12
|
+
const applyUniform = (id: number, key: string, value: UniformValue): void => {
|
|
13
|
+
if (typeof value === "string") {
|
|
14
|
+
if (value.length > 7) _creator.setUniformRgba(id, key, parseInt(value.slice(1), 16))
|
|
15
|
+
else _creator.setUniformRgb(id, key, parseInt(value.slice(1), 16))
|
|
16
|
+
} else if (typeof value === "number") {
|
|
17
|
+
_creator.setUniformFloat(id, key, value)
|
|
18
|
+
} else if (typeof value === "boolean") {
|
|
19
|
+
_creator.setUniformBoolean(id, key, value)
|
|
20
|
+
} else if (value instanceof Texture) {
|
|
21
|
+
_creator.setUniformTexture(id, key, value._id, value.wrapS, value.wrapT)
|
|
22
|
+
} else if (value instanceof Float32Array) {
|
|
23
|
+
_creator.setUniformArray(id, key, value)
|
|
24
|
+
} else if (Array.isArray(value)) {
|
|
25
|
+
_creator.setUniformArray(id, key, Float32Array.of(...value))
|
|
26
|
+
} else {
|
|
27
|
+
_creator.setUniformNull(id, key)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type MaterialColorOptions = { color?: ColorInput, map?: Texture | null }
|
|
32
|
+
|
|
33
|
+
export class Material {
|
|
34
|
+
/** @internal native material-instance handle. */
|
|
35
|
+
_id: number
|
|
36
|
+
readonly shader: FetchResponse | "unknown"
|
|
37
|
+
readonly uniforms: Record<string, UniformValue>
|
|
38
|
+
|
|
39
|
+
private _colorKey: string | null = null
|
|
40
|
+
private _useMapFlag = true
|
|
41
|
+
|
|
42
|
+
constructor(shader: FetchResponse | number) {
|
|
43
|
+
if (typeof shader === "number") {
|
|
44
|
+
this.shader = "unknown"
|
|
45
|
+
this._id = shader
|
|
46
|
+
} else {
|
|
47
|
+
this.shader = shader
|
|
48
|
+
this._id = _creator.createMaterial((shader as any)._id)
|
|
49
|
+
}
|
|
50
|
+
const store: Record<string, UniformValue> = {}
|
|
51
|
+
const id = () => this._id
|
|
52
|
+
this.uniforms = new Proxy(store, {
|
|
53
|
+
get: (t, k: string) => t[k],
|
|
54
|
+
set: (t, k: string, v: UniformValue) => { t[k] = v; applyUniform(id(), k, v); return true },
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Set a uniform (chainable). */
|
|
59
|
+
set(key: string, value: UniformValue): this {
|
|
60
|
+
this.uniforms[key] = value
|
|
61
|
+
return this
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
set color(c: ColorInput) {
|
|
65
|
+
if (this._colorKey) this.uniforms[this._colorKey] = Color.toHexString(c)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
set map(value: Texture | null) {
|
|
69
|
+
if (value === null) {
|
|
70
|
+
if (this._useMapFlag) this.uniforms.useBaseColorMap = false
|
|
71
|
+
} else {
|
|
72
|
+
this.uniforms.baseColorMap = value
|
|
73
|
+
if (this._useMapFlag) this.uniforms.useBaseColorMap = true
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// --- factories ---
|
|
78
|
+
|
|
79
|
+
/** PBR lit material. */
|
|
80
|
+
static lit(options: MaterialColorOptions = {}): Material {
|
|
81
|
+
// The fetchLocal literal must be inline so the compiler's preload header captures the shader name.
|
|
82
|
+
const m = new Material({ _id: _creatorUtils.fetchLocal("lit.filamat") } as any)
|
|
83
|
+
m._colorKey = "baseColor"
|
|
84
|
+
if (options.color !== undefined) m.color = options.color
|
|
85
|
+
if (options.map) m.map = options.map
|
|
86
|
+
return m
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Unlit (flat) material. */
|
|
90
|
+
static unlit(options: MaterialColorOptions = {}): Material {
|
|
91
|
+
const m = new Material({ _id: _creatorUtils.fetchLocal("unlit.filamat") } as any)
|
|
92
|
+
m._colorKey = "baseColor"
|
|
93
|
+
if (options.color !== undefined) m.color = options.color
|
|
94
|
+
if (options.map) m.map = options.map
|
|
95
|
+
return m
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Material that samples a VideoPlayer's texture. */
|
|
99
|
+
static video(map?: Texture): Material {
|
|
100
|
+
const m = new Material({ _id: _creatorUtils.fetchLocal("video.filamat") } as any)
|
|
101
|
+
m._useMapFlag = false
|
|
102
|
+
if (map) m.uniforms.baseColorMap = map
|
|
103
|
+
return m
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Shadow-catcher material (transparent except where shadows fall). */
|
|
107
|
+
static shadow(color: ColorInput = "#000000aa"): Material {
|
|
108
|
+
const m = new Material({ _id: _creatorUtils.fetchLocal("shadow.filamat") } as any)
|
|
109
|
+
m._colorKey = "color"
|
|
110
|
+
m.color = color
|
|
111
|
+
return m
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Load a custom compiled shader (.mat URL) as a material. */
|
|
115
|
+
static load(url: string): Promise<Material> {
|
|
116
|
+
return fetch(url, { useOnce: true }).then((resp) => {
|
|
117
|
+
if (resp.status >= 400) return Promise.reject(new Error(`Failed to fetch shader from ${url}. HTTP ${resp.status}`))
|
|
118
|
+
return new Material(resp)
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** @internal */
|
|
123
|
+
static idOf(material: Material): number { return material._id }
|
|
124
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// A renderable 3D node from raw geometry — the primitive shapes (box/sphere/cylinder/plane) and
|
|
2
|
+
// Mesh.from for a custom Geometry. A GLB import is a different kind: see Model.load.
|
|
3
|
+
|
|
4
|
+
import { cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
5
|
+
import {
|
|
6
|
+
Geometry,
|
|
7
|
+
type CylinderOptions,
|
|
8
|
+
type PlaneOptions,
|
|
9
|
+
type SphereOptions,
|
|
10
|
+
} from "./Geometry"
|
|
11
|
+
import { Material } from "./Material"
|
|
12
|
+
import { Node } from "./Node"
|
|
13
|
+
|
|
14
|
+
/** Common transform/render options every primitive factory accepts. */
|
|
15
|
+
export type MeshOptions = {
|
|
16
|
+
material?: Material
|
|
17
|
+
position?: Vec3Like
|
|
18
|
+
eulerAngles?: Vec3Like
|
|
19
|
+
scale?: Vec3Like | number
|
|
20
|
+
name?: string
|
|
21
|
+
castShadows?: boolean
|
|
22
|
+
receiveShadows?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class Mesh extends Node {
|
|
26
|
+
private _geometry?: Geometry
|
|
27
|
+
private _materials: Material[] = []
|
|
28
|
+
|
|
29
|
+
constructor(geometry?: Geometry, material?: Material) {
|
|
30
|
+
super()
|
|
31
|
+
if (geometry) {
|
|
32
|
+
const mat = material ?? Material.unlit()
|
|
33
|
+
this._geometry = geometry
|
|
34
|
+
this._materials = [ mat ]
|
|
35
|
+
_creator.setMesh(this.id, Material.idOf(mat), geometry.vertices, geometry.normals, geometry.indices, geometry.uv, geometry._meshKind)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
override get geometry(): Geometry | undefined { return this._geometry }
|
|
40
|
+
|
|
41
|
+
get material(): Material { return this._materials[0] }
|
|
42
|
+
set material(m: Material) { this.setMaterial(m, 0) }
|
|
43
|
+
|
|
44
|
+
setMaterial(material: Material, index = 0): this {
|
|
45
|
+
this._materials[index] = material
|
|
46
|
+
_creator.setMaterial(this.id, Material.idOf(material), index)
|
|
47
|
+
return this
|
|
48
|
+
}
|
|
49
|
+
getMaterial(index = 0): Material | null {
|
|
50
|
+
const existing = this._materials[index]
|
|
51
|
+
if (existing) return existing
|
|
52
|
+
const id = _creator.getMaterial(this.id, index)
|
|
53
|
+
if (id === 0) return null
|
|
54
|
+
const mat = new Material(id)
|
|
55
|
+
this._materials[index] = mat
|
|
56
|
+
return mat
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
set culling(v: boolean) { _creator.setCulling(this.id, v) }
|
|
60
|
+
set castShadows(v: boolean) { _creator.setCastShadows(this.id, v) }
|
|
61
|
+
set receiveShadows(v: boolean) { _creator.setReceiveShadows(this.id, v) }
|
|
62
|
+
|
|
63
|
+
// --- factories ---
|
|
64
|
+
|
|
65
|
+
static box(options: MeshOptions & { size?: Vec3Like | number } = {}): Mesh {
|
|
66
|
+
const geo = Geometry.box()
|
|
67
|
+
const [ sx, sy, sz ] = sizeToVec(options.size ?? 1)
|
|
68
|
+
if (sx !== 1 || sy !== 1 || sz !== 1) geo.scale(sx, sy, sz)
|
|
69
|
+
return apply(new Mesh(geo, options.material), options)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static sphere(options: MeshOptions & SphereOptions = {}): Mesh {
|
|
73
|
+
return apply(new Mesh(Geometry.sphere(options), options.material), options)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static cylinder(options: MeshOptions & CylinderOptions = {}): Mesh {
|
|
77
|
+
return apply(new Mesh(Geometry.cylinder(options), options.material), options)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static plane(options: MeshOptions & PlaneOptions = {}): Mesh {
|
|
81
|
+
return apply(new Mesh(Geometry.plane(options), options.material), options)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Wrap a custom Geometry. */
|
|
85
|
+
static from(geometry: Geometry, options: MeshOptions = {}): Mesh {
|
|
86
|
+
return apply(new Mesh(geometry, options.material), options)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const sizeToVec = (s: Vec3Like | number): [number, number, number] => (typeof s === "number" ? [ s, s, s ] : [ cx(s), cy(s), cz(s) ])
|
|
91
|
+
|
|
92
|
+
const apply = (mesh: Mesh, o: MeshOptions): Mesh => {
|
|
93
|
+
if (o.position) mesh.position = o.position
|
|
94
|
+
if (o.eulerAngles) mesh.eulerAngles = o.eulerAngles
|
|
95
|
+
if (o.scale !== undefined) mesh.scale = typeof o.scale === "number" ? [ o.scale, o.scale, o.scale ] : o.scale
|
|
96
|
+
if (o.name !== undefined) mesh.name = o.name
|
|
97
|
+
if (o.castShadows !== undefined) mesh.castShadows = o.castShadows
|
|
98
|
+
if (o.receiveShadows !== undefined) mesh.receiveShadows = o.receiveShadows
|
|
99
|
+
return mesh
|
|
100
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// A loaded GLB model — its own node kind (a GLB is a node hierarchy with baked animation clips),
|
|
2
|
+
// distinct from Mesh (raw primitives, no animation). Animation is always present, reached as
|
|
3
|
+
// model.anim:
|
|
4
|
+
// const hero = await Model.load(asset('./hero.glb'))
|
|
5
|
+
// hero.anim.play('Run')
|
|
6
|
+
|
|
7
|
+
import { fetch, type FetchResponse } from "../runtime/fetch"
|
|
8
|
+
import { Node } from "./Node"
|
|
9
|
+
import { ModelAnimation } from "./ModelAnimation"
|
|
10
|
+
|
|
11
|
+
export class Model extends Node {
|
|
12
|
+
/** GLB skeletal animation — always present on a Model. */
|
|
13
|
+
declare readonly anim: ModelAnimation
|
|
14
|
+
|
|
15
|
+
constructor(internalId: number) {
|
|
16
|
+
super(internalId)
|
|
17
|
+
this.aspect(ModelAnimation)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Load a GLB model. Returns its root as a Model; play its baked clips via model.anim. */
|
|
21
|
+
static load(
|
|
22
|
+
source: string | FetchResponse,
|
|
23
|
+
options: { culling?: boolean, onProgress?: (p: { loaded: number, total?: number }) => void } = {},
|
|
24
|
+
): Promise<Model> {
|
|
25
|
+
if (typeof source === "string") {
|
|
26
|
+
return fetch(source, { useOnce: true, onProgress: options.onProgress }).then((resp) => {
|
|
27
|
+
if (resp.status >= 400) return Promise.reject(new Error(`Failed to load GLB from ${source}. HTTP ${resp.status}`))
|
|
28
|
+
return Model.load(resp, options)
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
return new Promise<Model>((resolve, reject) => {
|
|
32
|
+
_creator.createGlb((source as unknown as { _id: number })._id, (entityId: number) => {
|
|
33
|
+
if (entityId === 0) { reject(new Error("Failed to load GLB")); return }
|
|
34
|
+
const model = new Model(entityId)
|
|
35
|
+
if (options.culling !== true) _creator.setGlbCulling(entityId, false)
|
|
36
|
+
resolve(model)
|
|
37
|
+
}, reject)
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// GLB skeletal animation as an aspect. Auto-attached by Model (a glb always has its clips), so you
|
|
2
|
+
// reach it directly: model.anim.play('Run'). Clips come baked in the glb; play by name or index.
|
|
3
|
+
|
|
4
|
+
import { Aspect } from "../core/Aspect"
|
|
5
|
+
import { nodeRegistry } from "./Node"
|
|
6
|
+
import type { Model } from "./Model"
|
|
7
|
+
|
|
8
|
+
let animatorRegistered = false
|
|
9
|
+
const ensureAnimator = (): void => {
|
|
10
|
+
if (animatorRegistered) return
|
|
11
|
+
animatorRegistered = true
|
|
12
|
+
_creator.setOnAnimationComplete((entityId, clipIndex) => nodeRegistry.get(entityId)?._onAnimationComplete(clipIndex) ?? true)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class ModelAnimation extends Aspect<"anim", Model> {
|
|
16
|
+
static readonly aspect = "anim"
|
|
17
|
+
|
|
18
|
+
private _playing = false
|
|
19
|
+
private _clip = 0
|
|
20
|
+
private _speed = 1
|
|
21
|
+
private _loop = false
|
|
22
|
+
|
|
23
|
+
onAttach(): void {
|
|
24
|
+
this.node._animComplete = (clipIndex) => {
|
|
25
|
+
if (clipIndex !== this._clip) return true
|
|
26
|
+
if (!this._loop) { this._playing = false; this.node._emitAnim("completed", clipIndex) }
|
|
27
|
+
else this.node._emitAnim("loopReached", clipIndex)
|
|
28
|
+
return this._playing
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The clips baked into the glb. */
|
|
33
|
+
get clips(): { name: string, duration: number }[] { return _creator.getGlbAnimations(this.node.id) }
|
|
34
|
+
|
|
35
|
+
get playing(): boolean { return this._playing }
|
|
36
|
+
set playing(value: boolean) {
|
|
37
|
+
this._playing = value
|
|
38
|
+
_creator.setAnimation(this.node.id, value, this._clip, this._speed)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Play a clip by name or index. Chainable. */
|
|
42
|
+
play(clip?: string | number, options: { loop?: boolean } = {}): this {
|
|
43
|
+
if (typeof clip === "number") this._clip = clip
|
|
44
|
+
else if (typeof clip === "string") {
|
|
45
|
+
const i = this.clips.findIndex((c) => c.name === clip)
|
|
46
|
+
if (i >= 0) this._clip = i
|
|
47
|
+
}
|
|
48
|
+
if (options.loop !== undefined) this._loop = options.loop
|
|
49
|
+
this.playing = true
|
|
50
|
+
ensureAnimator()
|
|
51
|
+
return this
|
|
52
|
+
}
|
|
53
|
+
stop(): this { this.playing = false; return this }
|
|
54
|
+
|
|
55
|
+
get loop(): boolean { return this._loop }
|
|
56
|
+
set loop(v: boolean) { this._loop = v }
|
|
57
|
+
get speed(): number { return this._speed }
|
|
58
|
+
set speed(v: number) {
|
|
59
|
+
this._speed = v
|
|
60
|
+
_creator.setAnimation(this.node.id, this._playing, this._clip, v)
|
|
61
|
+
}
|
|
62
|
+
get time(): number { return _creator.getAnimationTime(this.node.id) }
|
|
63
|
+
set time(v: number) { _creator.setAnimationTime(this.node.id, this._clip, v) }
|
|
64
|
+
}
|