@jgengine/assets 0.25.0 → 0.27.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/CHANGELOG.md +109 -0
- package/dist/catalogs/materialLibrary.d.ts +61 -0
- package/dist/catalogs/materialLibrary.js +173 -0
- package/dist/cli/pull.js +14 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/materialMapFiles.d.ts +15 -0
- package/dist/materialMapFiles.js +13 -0
- package/dist/materials.d.ts +5 -12
- package/dist/materials.js +4 -9
- package/dist/sources/ambientcg.d.ts +7 -1
- package/dist/sources/ambientcg.js +91 -2
- package/package.json +2 -2
- package/skills/jgengine/api.md +59 -6
- package/skills/jgengine/capabilities.md +4 -0
- package/skills/jgengine-assets/SKILL.md +10 -1
- package/skills/jgengine-assets/api.md +41 -9
package/CHANGELOG.md
CHANGED
|
@@ -26,6 +26,115 @@ At publish, rename this heading to the new version and mirror the entries into
|
|
|
26
26
|
`packages/core/src/meta/changelog.ts` (the typed `CHANGELOG` export).
|
|
27
27
|
-->
|
|
28
28
|
|
|
29
|
+
## 0.27.0
|
|
30
|
+
|
|
31
|
+
### Migrate
|
|
32
|
+
|
|
33
|
+
- **Bump lockstep SDK packages to `^0.27.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.24.0`; `@jgengine/github` is `0.14.0`.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `rollLoot` (`@jgengine/core/loot/lootGen`) rolls a named item from a caller `LootCatalog`: bases with mount slots, parts, affix pools, weighted rarity tiers, manufacturer personalities and a `levelScale` curve. Stats are `base × rarity.statScale × levelScale`, then part stats/multipliers, maker `bias`/`scale`, then additive and multiplicative affixes; additive affix rolls scale with level, multiplicative ones do not.
|
|
38
|
+
- `createLootRoller` wraps a validated catalog with roll counts per rarity, `snapshot()`/`restore()`/`reset()` and an optional devtools probe; `assertLootCatalog` rejects duplicate ids, unresolvable pools, empty item slots and a non-ascending level curve at construction.
|
|
39
|
+
- `validateLootItem` returns coded issues (`unknown-base`, `manufacturer-base`, `missing-slot-part`, `part-category`, `unknown-affix`, `empty-name`, …) and `compareLootItems` returns sorted per-stat deltas plus a weighted score with caller-named `lowerIsBetter` stats.
|
|
40
|
+
- `lootWeaponDefinition` (`@jgengine/core/loot/lootWeapon`) projects a rolled item onto a `WeaponDefinition` through a caller stat map, so a generated gun runs in `createWeaponSystem` after `assertWeaponDefinition`.
|
|
41
|
+
- `defineXpCurve` (`@jgengine/core/gameplay`) resolves one authored growth rule (`flat`, `linear`, `power`, `geometric`, `table`) plus a `maxLevel` cap into per-level thresholds, lifetime totals, a plain-words rule string and a `leveling` track.
|
|
42
|
+
- `createSkillTree` spends and refunds points over authored nodes with `cost`, `requires`, `excludes`, passives, actives and multi-node `synergies`; `resolved()` returns the stat modifiers and actions, and state snapshots/restores as `{ points, ranks }`.
|
|
43
|
+
- `gearScore` / `compareGearSets` score a whole loadout from stat weights, level, rarity and slot coverage.
|
|
44
|
+
- `lootStatContributions` and `scoreLootStats` expose the weight policy `compareLootItems` already used.
|
|
45
|
+
- `createEquipment` (`@jgengine/core/inventory/equipment`) wears gear by slot: slot defs declare accepted kinds and the slots they block, item defs declare the slots an item may occupy, a filled slot swaps and hands back the replaced item, and a two-handed item and the slot it blocks cannot both be filled. `state()` / `restore()` round-trip the worn set; a devtools probe reports filled slots.
|
|
46
|
+
- `characterLoad` and `canCarry` (`@jgengine/core/inventory/carryLoad`) weigh carried inventories plus worn gear against a capacity and hand back the shared encumbrance bands (tier, `moveMultiplier`, sprint/jump gates, `remaining`), so a stash left out of `carried` adds no weight.
|
|
47
|
+
- `fitShapedItem` (`@jgengine/core/inventory/shapedGrid`) places a shaped item at its first fit or names why not: `too-large` (with both sizes), `no-space`, `budget-exceeded`.
|
|
48
|
+
- `InventorySet.snapshot()` / `restore()` / `reset()` (`@jgengine/core/inventory/inventoryModel`) save and reload every declared inventory as detached plain data, so a bag and a stash never share live slots.
|
|
49
|
+
- `splitStack` takes an optional stack `limit`, rejecting `over-stack` instead of merging past the max stack.
|
|
50
|
+
- `createLaneGraph` and `laneGraphFromDocument` (`@jgengine/core/world`) read an authored lane graph out of the scene document: `traffic_lane` paths are one directed span each, `road` paths (the street bake) become a lane per direction split where streets cross, `traffic_intersection` markers carry signal phases and right-of-way in `meta`, and `traffic_parking` markers place kerbside spots.
|
|
51
|
+
- `createTraffic` (`@jgengine/core/world`) runs cars on that graph: IDM car following and braking, one car at a time through each intersection box behind a signal phase or first-come right-of-way, lane changes across authored neighbours, parking that reserves a spot, deterministic injected variety, `snapshot`/`restore`, and a devtools probe.
|
|
52
|
+
- `Traffic.setDemand(count)` is the fleet target; the time-of-day curve stays the caller's policy.
|
|
53
|
+
- `applyVehicleParts` (`@jgengine/core/world`) composes data-driven tuning parts onto a `KinematicVehicleTuning` — stat multipliers/addends applied in install order plus an optional tire grip curve — and returns the composed tuning, the installed ids, and a `VehicleFeel` (top speed, drive, brakes, turn rate, handbrake grip, and the grip curve's peak/slide). `withPart` swaps a part within its slot and `feelOf` reads the numbers out of any tuning.
|
|
54
|
+
- `createVehicleDamage` (`@jgengine/core/world`) is fixed-zone crash damage wired to handling and visuals: impacts accumulate into discrete stages, each stage costs grip/drive/brakes/top speed/drag and pulls the steering (`effect`), and each zone shows one bounded dent (`deformation`) that never exceeds its authored `deform`. Parts can detach at a set stage, a total impulse can disable the vehicle, and `snapshot`/`restore` resume mid-crash. `damagedTuning` and `damagedAxis` fold the effect into a tuning or an input, and `modifiers()` carries it as per-tick overrides.
|
|
55
|
+
- `KinematicVehicleTuning.weightTransfer` (`@jgengine/core/world`) adds load-transfer grip: longitudinal transfer under braking/traction and lateral transfer under cornering cut the tire grip curve (tire load sensitivity), so braking into a corner holds less slide than coasting through it. Each tick reports `weightTransfer` and `loadGrip`; omitting the block keeps the previous flat-grip model exactly.
|
|
56
|
+
- `KinematicVehicleModifiers.gripScale` scales the grip curve for one tick, mirroring `VehicleDynamicsModifiers.gripScale` so a damage layer drives either sim.
|
|
57
|
+
- `decal` scene kind (`@jgengine/core/world/decalKind`) and its renderer (`@jgengine/shell/scene/DecalRenderer`): dirt, cracks, paint and blood decals authored as placed markers, draped on the live terrain or standing on an authored tilt, with a deterministic procedural texture per style+seed.
|
|
58
|
+
- `TerrainMaterialLayer.material` (`@jgengine/core/world/terraform`): a material library name whose scanned color map paints that terrain layer. The runtime samples up to four layer maps at each layer's `tiling` and blends them through the authored mask weights (`createTerrainDetailMaterial`'s `layerMaps`), so painted terrain blends between authored layers instead of tiling one flat map.
|
|
59
|
+
- `grass_field` volumes now paint a ground-cover mat under their blades (`groundCover` rule, `@jgengine/shell/terrain/GrassCover`), so an authored patch reads as turf rather than bare ground with stubble. `GrassEnvironmentConfig.groundCover` does the same for `grass()` patches.
|
|
60
|
+
- `ModelConfig.wind` (`@jgengine/core/game/playableGame`) and `applyModelWind` (`@jgengine/shell/render/modelWind`): per-model foliage sway with height-ramped travel, shared gust clock and per-model strength/speed/direction.
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- The player shell re-renders only for the ctx slices it reads — the possessed entity and the touch control mode — instead of on every `ctx` write. Subscribing the shell root to the raw ctx version reconciled the canvas, the whole actor tree and the HUD on each per-frame entity pose write, which is what `subscribeMembership` and the markers' imperative `useFrame` pose reads exist to avoid (#625). Scrap Signal: 86.6 ms → 16.7 ms per frame at the same 621 draws; Harbor Heat: 102.8 ms → 46.2 ms at the same ~7.2k draws.
|
|
65
|
+
- The `ocean()` environment feature passes the live ground to its shader, so the sea shades shallow→deep and its shoreline foam band lands where the bed meets the surface.
|
|
66
|
+
|
|
67
|
+
### Numbers
|
|
68
|
+
|
|
69
|
+
- Test catalog (60/25/10/4/1 rarity weights) over 10,000 drops at level 20, fixed seed: common 59.41% (target 60%), uncommon 25.66% (25%), rare 9.58% (10%), epic 4.20% (4%), legendary 1.15% (1%); every tier inside its 4σ binomial bound. All 10,000 items validated with zero issues and composed names (e.g. `Voss Rare Long Keen Lightframe of Reach`, `Tallow Long Lightframe`, `Voss Snub Lightframe of Steadying`).
|
|
70
|
+
- Level curve `[[1,1],[15,1.6],[30,2.4],[60,4]]`: a level-60 roll's base and additive affix values are 4× the same seed at level 1; maker personalities move the same roll (`Voss` +power/−cadence, `Tallow` the reverse).
|
|
71
|
+
- A rolled epic projects to a hitscan `WeaponDefinition` (rate, damage, integer magazine capacity, spread, range), passes `assertWeaponDefinition`, and its first `tick` fires one shot and one impact through `createDamageRuntime` with the rolled capacity spent.
|
|
72
|
+
|
|
73
|
+
## 0.26.0
|
|
74
|
+
|
|
75
|
+
### Migrate
|
|
76
|
+
|
|
77
|
+
- **Bump lockstep SDK packages to `^0.26.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.23.0`; `@jgengine/github` is `0.13.0`.
|
|
78
|
+
|
|
79
|
+
### Added
|
|
80
|
+
|
|
81
|
+
- `createLogBus` (`@jgengine/core/devtools/logBus`): a fixed-capacity ring of log lines tagged by owning system, with a separate ring for warnings and errors so a crash line is never pushed out by info traffic. `devtools.logs` now writes through it (`write(level, system, message)`, `snapshot()`, `counts()`, `dropped()`); `DevtoolsLogEntry` gained `system`.
|
|
82
|
+
- `devtools.probes.registerSystem(id, { counters, timers, state })`: a system reports its own counters, named timings and one state line in a few lines, with no shell wiring. Readings surface as `systems` in `devtools.snapshot()`, in `debug_snapshot`, and in every perf record.
|
|
83
|
+
- `PerfReport` (`@jgengine/core/devtools/report`): the one perf record shape shared by `bun run perf` and the in-page report.
|
|
84
|
+
- The shell samples frame telemetry in-page in dev builds: `window.__JG.report()` returns that record, and the page pushes it about once a second to the dev server's `GET/POST /__jgengine/report` route. `bun run debug <game>` prints the record of a game already open and exits non-zero on a budget breach in `scripts/perf-budget.json`.
|
|
85
|
+
- `createNeedSet` (`@jgengine/core/gameplay`) drains named needs on game time and reads them by index — decaying hunger, energy, fun with snapshot/restore and per-need rate retuning.
|
|
86
|
+
- `createUtilityBrain` (`@jgengine/core/world`) picks an action from decaying-need appeal: linear, quadratic, logistic, exponential and threshold response curves, candidate cost (travel, queues, price), a switch margin plus minimum hold (hysteresis), declared interrupts, optional seeded jitter, and a devtools probe string.
|
|
87
|
+
- `createNavRuntime` (`@jgengine/core/world`) consumes an editor-baked nav mesh: reads the `nav` bake off a scene document (`navMeshFromBake`), routes with the mesh query, resolves authored doors, ladders and jumps (`AuthoredNavLink`, priced by kind) onto the current surface, and swaps in a new bake after an edit while matching polygon identity by geometry — `applyMesh` returns a `remap` so only routes over changed polygons need re-planning.
|
|
88
|
+
- `createNavCrowd` (`@jgengine/core/world`) moves many agents across that mesh: plans a corridor, follows the funnel-smoothed waypoints, keeps agents apart with reciprocal velocity obstacle avoidance, bounds path searches per tick by count and milliseconds, fans converging agents out with `spread`, and round-trips through `snapshot`/`restore`.
|
|
89
|
+
- `createRvoCrowd` (`@jgengine/core/world`) is the avoidance solver on its own: RVO/ORCA half-plane solve over a uniform-grid neighbour search capped at `maxNeighbors`, deterministic tie-breaking, overlap relief, retunable `timeHorizon`/`neighborDist`/`maxNeighbors`/`sideBias`/`overlapRelief`, and `snapshot`/`restore` for saves and replays.
|
|
90
|
+
- `resolveRoutine` (`@jgengine/core/gameplay`) resolves an authored day into real arrival and departure times against the world clock. Travel time is injected per stop or as a callback, so a stop departs early enough to walk to the next one and an arrival that runs late slips instead of teleporting; `phaseAt(time)` returns the held place, the walking leg and its progress, and `nextChangeAfter(time)` drives a scheduler. A day whose walks cannot fit its cycle throws at construction.
|
|
91
|
+
- `createOffScreenWorld` (`@jgengine/core/gameplay`) keeps a whole population without simulating it: residents beyond the injected radius are cheap state (position, routine phase, needs) that only moves when the clock crosses a routine boundary, and they spawn into the game's own near simulation through a `NearAgents` interface when they come close. The radius, an optional per-agent cost model, a near budget and a per-tick switch budget are all injected; `snapshot`/`restore` round-trip the whole population, and crossing the boundary preserves identity, schedule progress, need values and position.
|
|
92
|
+
- `createNearAgents` (`@jgengine/core/gameplay`) is the default near half of that seam: a nav crowd with a need set and utility brain per agent, goal-tracking as routines move residents between places, and a decision counter for tests and probes.
|
|
93
|
+
- `createPerception` shares squad knowledge: `squadMates(observerId)` names an observer's roster, and a sighting one member makes is written into every mate's memory with `sourceId` (who saw it) and `sourceTick` (the observe pass it was recorded on). A mate's own sighting at the same instant replaces the hand-off. `maxSharesPerTick` (default 64, per observer pass) bounds a large roster and reports elided shares.
|
|
94
|
+
- Perception memory is auditable without guessing: `memoryOf(observerId, targetId)` returns the one remembered target with its confidence right now or `null` once it has faded, and `stats()` counts pair checks, range prunes, cone tests, line-of-sight calls, sightings, heard stimuli, shares and dropped shares. The check order is range → cone → line of sight, so occlusion work only sees the pruned set. `resetStats()` opens a measurement window; `memories` are pruned per observer so a 200-observer pass does not rescan every memory.
|
|
95
|
+
- `devtoolsName` registers a probe showing observers, remembered targets and the work counters, and `dispose()` removes it.
|
|
96
|
+
- `createCoverField` and `coverPointsFromSolids` (`@jgengine/core/combat`) derive stand-able cover from authored solid boxes instead of a coordinate list: one row of points per vertical face, clear of the corners, filtered by an injected `walkable` test, rebuilt when the world is edited, and queried through a bounded `near(position, radius, limit)`. `CoverSource` is the seam, so a game with its own spatial index can supply cover without the box generator.
|
|
97
|
+
- `selectCover` / `scoreCoverPoint` choose cover by threat: `hidden` is the confidence-weighted share of remembered threats whose line of sight the point (and a body-sized footprint at it) blocks, scored against the role's preferred fighting range, flank angle around the threat, distance, a squadmate's claim on the point, and whether the walk to it is a detour. Fading memories stop steering at `minConfidence`, and a point that hides from only one of two shooters is not cover.
|
|
98
|
+
- `createSquadCombat` (`@jgengine/core/combat`) drives a squad over existing seams: role-driven cover selection, fire discipline (hold, lean out, shoot), suppression from `underFire()`, retreat below a role's health threshold, and relocation under sustained fire. Threat comes only from `threatsFromMemory`, i.e. the perception memory the game already keeps; movement goes through `NavCrowd` with a tighter arrival tolerance so a body actually reaches the stand point. Serializable through `snapshot()`/`restore()` (member decisions, crowd, tuning), retunable, and a `devtoolsName` probe reports pins, peeks, shots and the worst exposure.
|
|
99
|
+
- `assignSquadRoles` and `threatsFromMemory` (`@jgengine/core/combat`): deterministic role assignment over a caller-authored `SquadComposition` (role ids, counts, ranges, flank angles, retreat thresholds and cover weights are all content), and a bounded, confidence-ranked threat list from one observer's memory.
|
|
100
|
+
- A 2v4 arena soak proves the behavior: 90 simulated seconds, 2700 ticks, 4 squad members against 2 scripted raiders, worst exposure 2.33 s (bar 3 s), 0 of 38 exposure spells over 3 s, per-tick cost p50 0.05 ms / p95 0.19 ms.
|
|
101
|
+
- `createDamageRuntime` (`@jgengine/core/combat`) composes a damage table, hit zones, the armor curve and carried statuses into one stateful model: `hit()` returns every stage of the number (`base × zone multiplier × type multiplier × (1 − armor mitigation)`), `tick()` runs status effects through the same table and armor, `timeToKill()` quotes a fight, and `table()` reads the whole chart. `snapshot()`/`restore()` carry registered targets and carried statuses; `retune()` swaps table, zones or status definitions without dropping either; a `devtoolsName` probe reports hits, crits, ticks and the last hit.
|
|
102
|
+
- A damage table (`combat/damageTable`) of damage type × resistance class, with `armor / (armor + halfValue)` mitigation — armor equal to the half value halves the hit — a per-type `bypassArmor`, and `readDamageTable` / `formatDamageTable` to read every row at given armor values. An unknown damage type throws instead of silently dealing 1×.
|
|
103
|
+
- Hit zones and crits (`combat/hitZones`): a zone carries a normal multiplier and an optional crit multiplier, `resolveHitZone` rolls once and returns one multiplier, and a crit replaces the zone multiplier rather than compounding with it. `hitZoneMultiplier` quotes the same number without a roll.
|
|
104
|
+
- `createStatusRuntime` (`combat/statusRuntime`): data-defined statuses with the existing `StatusStackPolicy` (`refresh`, stack to `max`, `replace`, `ignore`) as the only thing that changes stacks, per-tick effects of `{ kind: "damage", channel }` or `{ kind: "stat", stat }`, damage routed through an injected `applyDamage` so resistances and armor still apply, bounded ticks per call, and plain-data state out and back.
|
|
105
|
+
- Numbers from the tests: 25 base kinetic against a 150 HP target, 0.15 s between shots — 8 shots / 1.05 s unarmored, 20 shots / 2.85 s at 60 armor, 36 shots / 5.25 s at 200 armor; two stacked burns of 4 per tick deal 8, not 16; a 1.5× head zone with a 2.5× crit multiplier deals 2.5×, not 3.75×.
|
|
106
|
+
- `createWeaponSystem` (`@jgengine/core/combat`) runs one `WeaponDefinition`: single, automatic or burst fire, hitscan or ballistic projectiles, recoil that moves the aim the shot flies down, spread bloom, interrupted/resumed reloads, a readable ammo economy (`ammo()`, `stats()`), `snapshot()`/`restore()` and a devtools probe. Every hit, hitscan or projectile, resolves through `createDamageRuntime`. Pellet work is bounded per tick (`maxShotsPerTick`, default 4) and per weapon (`maxInFlight`, default 32).
|
|
107
|
+
- `WeaponDefinition` (`@jgengine/core/combat/weaponDefinition`) as plain JSON — rate, fire mode, delivery, magazine, handling, damage, falloff — with `assertWeaponDefinition`, `weaponIntervalMs`, `weaponDamageAt` and `weaponMaxInFlight`. A generated gun is the same shape.
|
|
108
|
+
- `Magazine.pauseReload()` / `resumeReload()` / `isPaused()` suspend a reload and keep its progress; a paused reload is not `isReloading()`, does not advance in `tick`, and round-trips through `snapshot()`/`restore()`.
|
|
109
|
+
- `environment.grading` on the scene document authors a per-game colour grade: shadow/midtone/highlight swatches plus tint, saturation and contrast bake into a small display-space LUT (`lutCubeFromGrading`, `colorGradeFromDocument` in `@jgengine/core/editor/environment`), edited in the Lighting workspace. The shell applies it from `editorLayers`, and a graded game needs no LUT asset or hardcoded pass.
|
|
110
|
+
- Clamped auto-exposure: `postProcessing.autoExposure: { min, max, adaptationSeconds } | false` meters the pre-tone-map frame every four frames toward 18% grey and stays inside the authored range. `WHITE_POINT` and `MID_GREY` in `@jgengine/core/render/colorPipeline` are the single documented calibration the pipeline uses.
|
|
111
|
+
- `GradeConfig.lut` also accepts a baked `{ cube: { size, data } }` LUT, sampled trilinearly after the numeric grade.
|
|
112
|
+
- `MATERIAL_LIBRARY` (`@jgengine/assets/catalogs/materialLibrary`) names 56 PBR surfaces — wet asphalt, painted metal, stucco, brick, wood, foliage with cut-out coverage and sheen, glass, water, sand, concrete and more — each mapping a name to an ambientCG source, a world tile size in metres and a physical response. `libraryMaterialAsset("asphalt/wet")` returns the editable `MaterialAsset` those names resolve to, and `filterLibraryMaterials` backs pickers. `assets pull library` pulls every pinned map set.
|
|
113
|
+
- `TriplanarAssetMaterial` / `useTriplanarAssetMaterial` / `applyTriplanarProjection` (`@jgengine/shell/render/triplanarMaterial`) render a material asset from world position, so world geometry without a `uv` attribute still gets colour, normal, roughness and AO maps. Tile size is world metres; instanced meshes project in instanced space.
|
|
114
|
+
- The editor's Materials workspace browses the library by name and writes the chosen entry into the document's material assets.
|
|
115
|
+
|
|
116
|
+
### Changed
|
|
117
|
+
|
|
118
|
+
- `createPerception` (`@jgengine/core/world`, also `@jgengine/core/sensor/perception`) takes `isSegmentClear` instead of `occluded`: the game supplies collision as a "nothing blocks this segment" test, and it is only called for pairs already inside range and cone. **Migration:** flip the predicate — a `occluded: (from, to) => blocked(from, to)` becomes `isSegmentClear: (from, to) => !blocked(from, to)`.
|
|
119
|
+
- `MagazineSnapshot` gains an optional `paused: true` field, present only while a reload is suspended, so existing snapshots and idle shapes are unchanged.
|
|
120
|
+
- AgX is the pipeline's tone map: `postProcessing.toneMapping` defaults to `"agx"`, and the `cinematic`, `photoreal`, `comic`, `retro` and `STUDIO_STAGE_POST` configs now pick it too. `"toon"` keeps `"linear"` on purpose. Games that authored `"aces"` explicitly are unchanged.
|
|
121
|
+
- The grade pass samples LUT tiles at texel centres and blends the blue axis by hand, so a supplied strip LUT no longer bleeds across slices.
|
|
122
|
+
|
|
123
|
+
### Fixed
|
|
124
|
+
|
|
125
|
+
- `bun run debug <game>` now finds the server `bun dev` started. The website dev server publishes the same checkout marker the runner does (`scripts/devServerMarker.ts`, written at listen, removed on shutdown, dev builds only), and the marker records the host that answers, since Vite's default `localhost` can be IPv6-only where `127.0.0.1` hangs.
|
|
126
|
+
- The website dev server serves `GET/POST /__jgengine/report` too, so a game opened at `/games/<id>` reports from the `/play` runner it embeds.
|
|
127
|
+
- The in-page sampler installs when the dev report route answers, not on a build-time `PROD` check: the `/play` runner is a production build, so the old check silently disabled the report on `bun dev`.
|
|
128
|
+
- A report taken inside the sampler's warmup is no longer judged on link/framebuffer counts, which only mean something after the warmup baseline exists.
|
|
129
|
+
- `extractMaterialMaps` and `buildMaterialCatalog` now recognise ambientCG's `_Opacity` map, so cut-out foliage and decal scans keep their coverage instead of rendering as solid cards.
|
|
130
|
+
- Capture runs no longer leak a ~150 MB temp Chrome profile each. `shoot`, `drive` and `perf` delete their profile on every exit path — success, throw, watchdog and SIGINT — `bun run shoot` reclaims profiles a killed run left untouched for three hours, the capture guard sweeps the profile of a run it force-kills, and the generated `browser.mjs` harness for game repos removes its own. A capture also refuses to start when the temp drive has less free space than it needs.
|
|
131
|
+
- The post-processing chain, graphics settings and textured terrain material keep a stable identity across renders. Before, every render rebuilt the effect composer and recompiled shaders, which dropped games to single-digit FPS.
|
|
132
|
+
|
|
133
|
+
### Numbers
|
|
134
|
+
|
|
135
|
+
- Test fixture at 600 rpm, 30-round magazine: 2.9 s to empty; 17.0° pitch and 3.1° yaw climb over the magazine; back to half climb in 0.37 s and within 5% in 0.6 s; bloom 2.0 → 8.0 → 12.0 mrad (cap) by the tenth shot, settling to base 0.63 s after the last shot.
|
|
136
|
+
- A hitscan and a projectile contact at the same distance return the same damage (20 body, 30 on a 1.5x head zone) through one damage model; a 60 m/s projectile crosses 20 m in 0.33 s and drops 2.45 m in 0.5 s at 9.81 m/s².
|
|
137
|
+
|
|
29
138
|
## 0.25.0
|
|
30
139
|
|
|
31
140
|
### Migrate
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type MaterialAsset, type MaterialSurfaceParameters, type MaterialTextureMetadata, type MaterialTextureRole } from "@jgengine/core/material/materialAsset";
|
|
2
|
+
/** How a library surface renders: an ordinary PBR surface, cut-out foliage, glass or water. */
|
|
3
|
+
export type LibrarySurface = "opaque" | "foliage" | "glass" | "water";
|
|
4
|
+
/** Browse groups the library browser and `assets list` share. */
|
|
5
|
+
export type LibraryGroup = "asphalt" | "concrete" | "stucco" | "brick" | "tile" | "metal" | "wood" | "foliage" | "ground" | "fabric" | "stone" | "glass" | "water";
|
|
6
|
+
/**
|
|
7
|
+
* One named material a game or the editor selects by name. `source` names the ambientCG catalog
|
|
8
|
+
* entry whose pulled maps it uses; glass and water carry response parameters only, because
|
|
9
|
+
* ambientCG publishes no scanned glass or water material and both are shader surfaces.
|
|
10
|
+
*/
|
|
11
|
+
export interface LibraryMaterial {
|
|
12
|
+
/** Stable name, e.g. `asphalt/wet`. Names are the data contract; source ids and URLs are not. */
|
|
13
|
+
name: string;
|
|
14
|
+
/** Human label for pickers and contact sheets. */
|
|
15
|
+
title: string;
|
|
16
|
+
group: LibraryGroup;
|
|
17
|
+
/** ambientCG source id in the material catalog, whose maps `assets pull <id>` writes. */
|
|
18
|
+
source?: string;
|
|
19
|
+
surface: LibrarySurface;
|
|
20
|
+
/** Metres one texture tile covers on world geometry. */
|
|
21
|
+
tileMetres: number;
|
|
22
|
+
/** Sparse physical response merged over the imported or authored slot. */
|
|
23
|
+
response: MaterialSurfaceParameters;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The shared library: 40+ ambientCG PBR materials plus the glass and water surfaces that need a
|
|
27
|
+
* shader rather than a scan. `satisfies` keeps every entry complete, so a missing tile size or
|
|
28
|
+
* response is a type error instead of a silently grey surface.
|
|
29
|
+
*/
|
|
30
|
+
export declare const MATERIAL_LIBRARY: readonly LibraryMaterial[];
|
|
31
|
+
/** Map roles a library scan can carry: the PBR set plus cut-out coverage. */
|
|
32
|
+
export type LibraryMapRole = "color" | "normal" | "roughness" | "ao" | "opacity";
|
|
33
|
+
/** Resolve a library name; unknown names return `null` so a caller can report them by name. */
|
|
34
|
+
export declare function findLibraryMaterial(name: string): LibraryMaterial | null;
|
|
35
|
+
/** Every library name, in authored order. */
|
|
36
|
+
export declare function libraryMaterialNames(): readonly string[];
|
|
37
|
+
/** Map roles the pinned archive of `entry` actually ships, from its catalog source declaration. */
|
|
38
|
+
export declare function libraryMaterialRoles(entry: LibraryMaterial): readonly LibraryMapRole[];
|
|
39
|
+
/** Options for {@link libraryMaterialMaps} and {@link libraryMaterialAsset}. */
|
|
40
|
+
export interface LibraryMaterialOptions {
|
|
41
|
+
/** URL prefix `assets pull` writes under; defaults to `/materials`. */
|
|
42
|
+
basePath?: string;
|
|
43
|
+
/** How one texture tile is projected: authored UVs, or world-space triplanar for geometry without UVs. */
|
|
44
|
+
projection?: "uv" | "triplanar";
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Serialized runtime map URLs for a library material, matching what `assets pull <source>` writes
|
|
48
|
+
* into `<basePath>/<source-id>/`. Roles the archive does not ship are omitted, never invented.
|
|
49
|
+
*/
|
|
50
|
+
export declare function libraryMaterialMaps(entry: LibraryMaterial, options?: LibraryMaterialOptions): Partial<Record<MaterialTextureRole, MaterialTextureMetadata>>;
|
|
51
|
+
/**
|
|
52
|
+
* Build the editable, serializable material asset for a library name: maps, response and
|
|
53
|
+
* CC0 attribution in one value the editor can save into `editor.scene.json` and the shell renders.
|
|
54
|
+
* `projection` records whether those maps should sample authored UVs or world-space triplanar.
|
|
55
|
+
*/
|
|
56
|
+
export declare function libraryMaterialAsset(name: string, options?: LibraryMaterialOptions): MaterialAsset;
|
|
57
|
+
/**
|
|
58
|
+
* Filter the library for a picker: case-insensitive match on name, title, group or source.
|
|
59
|
+
* Empty query returns everything, so a browser can render the full library first.
|
|
60
|
+
*/
|
|
61
|
+
export declare function filterLibraryMaterials(query: string, group?: LibraryGroup): readonly LibraryMaterial[];
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { MATERIAL_TEXTURE_SEMANTICS, materialCapabilitiesForAsset, validateMaterialAsset, } from "@jgengine/core/material/materialAsset";
|
|
2
|
+
import { MATERIAL_MAP_FILES } from "../materialMapFiles.js";
|
|
3
|
+
import { sourceById } from "../sources/index.js";
|
|
4
|
+
const MATTE = { metalness: 0, roughness: 0.85 };
|
|
5
|
+
/**
|
|
6
|
+
* The shared library: 40+ ambientCG PBR materials plus the glass and water surfaces that need a
|
|
7
|
+
* shader rather than a scan. `satisfies` keeps every entry complete, so a missing tile size or
|
|
8
|
+
* response is a type error instead of a silently grey surface.
|
|
9
|
+
*/
|
|
10
|
+
export const MATERIAL_LIBRARY = [
|
|
11
|
+
// Asphalt and road — wet asphalt is the same scan with a low roughness, not a second download.
|
|
12
|
+
{ name: "asphalt/wet", title: "Wet asphalt", group: "asphalt", source: "ambientcg-asphalt025c", surface: "opaque", tileMetres: 4, response: { metalness: 0, roughness: 0.34, color: "#8f9498" } },
|
|
13
|
+
{ name: "asphalt/dry", title: "Dry asphalt", group: "asphalt", source: "ambientcg-asphalt003", surface: "opaque", tileMetres: 4, response: { metalness: 0, roughness: 0.82 } },
|
|
14
|
+
{ name: "asphalt/patch", title: "Patched asphalt", group: "asphalt", source: "ambientcg-asphalt021", surface: "opaque", tileMetres: 3.2, response: { metalness: 0, roughness: 0.74 } },
|
|
15
|
+
{ name: "road/tarmac", title: "Tarmac road", group: "asphalt", source: "ambientcg-road001", surface: "opaque", tileMetres: 6, response: { metalness: 0, roughness: 0.7 } },
|
|
16
|
+
{ name: "road/marked", title: "Marked road", group: "asphalt", source: "ambientcg-road008a", surface: "opaque", tileMetres: 8, response: { metalness: 0, roughness: 0.62 } },
|
|
17
|
+
// Concrete.
|
|
18
|
+
{ name: "concrete/smooth", title: "Smooth concrete", group: "concrete", source: "ambientcg-concrete006", surface: "opaque", tileMetres: 3, response: { metalness: 0, roughness: 0.78 } },
|
|
19
|
+
{ name: "concrete/poured", title: "Poured concrete", group: "concrete", source: "ambientcg-concrete013", surface: "opaque", tileMetres: 3.5, response: { metalness: 0, roughness: 0.88 } },
|
|
20
|
+
{ name: "concrete/rough", title: "Rough concrete", group: "concrete", source: "ambientcg-concrete025", surface: "opaque", tileMetres: 2.6, response: { metalness: 0, roughness: 0.94 } },
|
|
21
|
+
// Stucco and plaster.
|
|
22
|
+
{ name: "stucco/painted", title: "Painted stucco", group: "stucco", source: "ambientcg-paintedplaster006", surface: "opaque", tileMetres: 2.2, response: { metalness: 0, roughness: 0.9 } },
|
|
23
|
+
{ name: "stucco/warm", title: "Warm stucco", group: "stucco", source: "ambientcg-paintedplaster010", surface: "opaque", tileMetres: 2.2, response: { metalness: 0, roughness: 0.88 } },
|
|
24
|
+
{ name: "plaster/interior", title: "Interior plaster", group: "stucco", source: "ambientcg-plaster001", surface: "opaque", tileMetres: 2, response: { metalness: 0, roughness: 0.92 } },
|
|
25
|
+
// Brick.
|
|
26
|
+
{ name: "brick/red", title: "Red brick", group: "brick", source: "ambientcg-bricks004", surface: "opaque", tileMetres: 1.6, response: { metalness: 0, roughness: 0.86 } },
|
|
27
|
+
{ name: "brick/painted", title: "Painted brick", group: "brick", source: "ambientcg-paintedbricks002", surface: "opaque", tileMetres: 1.6, response: { metalness: 0, roughness: 0.8 } },
|
|
28
|
+
{ name: "brick/whitewash", title: "Whitewashed brick", group: "brick", source: "ambientcg-paintedbricks005", surface: "opaque", tileMetres: 1.6, response: { metalness: 0, roughness: 0.84 } },
|
|
29
|
+
// Tile, paving and roof.
|
|
30
|
+
{ name: "tile/ceramic", title: "Ceramic tile", group: "tile", source: "ambientcg-tiles007", surface: "opaque", tileMetres: 1.2, response: { metalness: 0, roughness: 0.32 } },
|
|
31
|
+
{ name: "paving/stone", title: "Stone paving", group: "tile", source: "ambientcg-pavingstones002", surface: "opaque", tileMetres: 2.4, response: { metalness: 0, roughness: 0.83 } },
|
|
32
|
+
{ name: "paving/sett", title: "Setts", group: "tile", source: "ambientcg-pavingstones020", surface: "opaque", tileMetres: 1.8, response: { metalness: 0, roughness: 0.8 } },
|
|
33
|
+
{ name: "roof/tiles", title: "Roof tiles", group: "tile", source: "ambientcg-roofingtiles006", surface: "opaque", tileMetres: 1.4, response: { metalness: 0, roughness: 0.78 } },
|
|
34
|
+
// Metal.
|
|
35
|
+
{ name: "metal/painted", title: "Painted metal", group: "metal", source: "ambientcg-paintedmetal006", surface: "opaque", tileMetres: 1.2, response: { metalness: 0.75, roughness: 0.44 } },
|
|
36
|
+
{ name: "metal/chipped", title: "Chipped paint", group: "metal", source: "ambientcg-paintedmetal002", surface: "opaque", tileMetres: 1, response: { metalness: 0.8, roughness: 0.5 } },
|
|
37
|
+
{ name: "metal/steel", title: "Bare steel", group: "metal", source: "ambientcg-metal007", surface: "opaque", tileMetres: 1, response: { metalness: 0.95, roughness: 0.42 } },
|
|
38
|
+
{ name: "metal/brushed", title: "Brushed metal", group: "metal", source: "ambientcg-metal032", surface: "opaque", tileMetres: 1, response: { metalness: 1, roughness: 0.3 } },
|
|
39
|
+
{ name: "metal/corrugated", title: "Corrugated steel", group: "metal", source: "ambientcg-corrugatedsteel009", surface: "opaque", tileMetres: 2, response: { metalness: 0.9, roughness: 0.55 } },
|
|
40
|
+
{ name: "metal/diamond-plate", title: "Diamond plate", group: "metal", source: "ambientcg-diamondplate005a", surface: "opaque", tileMetres: 1.2, response: { metalness: 0.92, roughness: 0.38 } },
|
|
41
|
+
{ name: "metal/rust", title: "Rusted metal", group: "metal", source: "ambientcg-rust007", surface: "opaque", tileMetres: 1.4, response: { metalness: 0.35, roughness: 0.88 } },
|
|
42
|
+
{ name: "metal/plate", title: "Steel plate", group: "metal", source: "ambientcg-metalplates001", surface: "opaque", tileMetres: 1.6, response: { metalness: 0.85, roughness: 0.5 } },
|
|
43
|
+
// Wood.
|
|
44
|
+
{ name: "wood/oak", title: "Oak", group: "wood", source: "ambientcg-wood026", surface: "opaque", tileMetres: 1.4, response: { metalness: 0, roughness: 0.66 } },
|
|
45
|
+
{ name: "wood/floor", title: "Wood floor", group: "wood", source: "ambientcg-woodfloor009", surface: "opaque", tileMetres: 1.8, response: { metalness: 0, roughness: 0.46 } },
|
|
46
|
+
{ name: "wood/deck", title: "Deck planks", group: "wood", source: "ambientcg-planks003", surface: "opaque", tileMetres: 1.6, response: { metalness: 0, roughness: 0.75 } },
|
|
47
|
+
{ name: "wood/painted", title: "Painted wood", group: "wood", source: "ambientcg-paintedwood006a", surface: "opaque", tileMetres: 1.2, response: { metalness: 0, roughness: 0.6 } },
|
|
48
|
+
{ name: "wood/siding", title: "Wood siding", group: "wood", source: "ambientcg-woodsiding005", surface: "opaque", tileMetres: 1.4, response: { metalness: 0, roughness: 0.72 } },
|
|
49
|
+
{ name: "wood/bark", title: "Tree bark", group: "wood", source: "ambientcg-bark002", surface: "opaque", tileMetres: 1, response: { metalness: 0, roughness: 0.92 } },
|
|
50
|
+
// Foliage — cut-out cards over a warm, backlit response.
|
|
51
|
+
{ name: "foliage/leaf", title: "Leaf card", group: "foliage", source: "ambientcg-leaf001", surface: "foliage", tileMetres: 0.8, response: { metalness: 0, roughness: 0.55, alphaMode: "mask", alphaCutoff: 0.45, doubleSided: true, sheen: 0.6, sheenColor: "#c8d18a", sheenRoughness: 0.7 } },
|
|
52
|
+
{ name: "foliage/leaf-dense", title: "Dense leaf card", group: "foliage", source: "ambientcg-leaf003", surface: "foliage", tileMetres: 1, response: { metalness: 0, roughness: 0.6, alphaMode: "mask", alphaCutoff: 0.45, doubleSided: true, sheen: 0.55, sheenColor: "#b9cc7d", sheenRoughness: 0.75 } },
|
|
53
|
+
{ name: "foliage/grass", title: "Grass ground", group: "foliage", source: "ambientcg-grass001", surface: "opaque", tileMetres: 2, response: { metalness: 0, roughness: 0.9 } },
|
|
54
|
+
{ name: "foliage/grass-dry", title: "Dry grass", group: "foliage", source: "ambientcg-grass004", surface: "opaque", tileMetres: 2, response: { metalness: 0, roughness: 0.92 } },
|
|
55
|
+
{ name: "foliage/moss", title: "Moss", group: "foliage", source: "ambientcg-moss001", surface: "opaque", tileMetres: 1.2, response: { metalness: 0, roughness: 0.95 } },
|
|
56
|
+
{ name: "foliage/leaf-litter", title: "Leaf litter", group: "foliage", source: "ambientcg-scatteredleaves008", surface: "opaque", tileMetres: 2.4, response: { metalness: 0, roughness: 0.88 } },
|
|
57
|
+
// Ground and nature.
|
|
58
|
+
{ name: "ground/dirt", title: "Forest floor", group: "ground", source: "ambientcg-ground037", surface: "opaque", tileMetres: 2.4, response: { metalness: 0, roughness: 0.9 } },
|
|
59
|
+
{ name: "ground/sand", title: "Beach sand", group: "ground", source: "ambientcg-ground055s", surface: "opaque", tileMetres: 2, response: { metalness: 0, roughness: 0.88 } },
|
|
60
|
+
{ name: "ground/sand-dark", title: "Dark sand", group: "ground", source: "ambientcg-ground080", surface: "opaque", tileMetres: 2, response: { metalness: 0, roughness: 0.86 } },
|
|
61
|
+
{ name: "ground/mud", title: "Wet mud", group: "ground", source: "ambientcg-ground095b", surface: "opaque", tileMetres: 2, response: { metalness: 0, roughness: 0.52 } },
|
|
62
|
+
{ name: "ground/gravel", title: "Gravel", group: "ground", source: "ambientcg-gravel002", surface: "opaque", tileMetres: 1.6, response: { metalness: 0, roughness: 0.88 } },
|
|
63
|
+
{ name: "ground/rock", title: "Rock face", group: "ground", source: "ambientcg-rock022", surface: "opaque", tileMetres: 3, response: { metalness: 0, roughness: 0.86 } },
|
|
64
|
+
{ name: "ground/ice", title: "Ice", group: "ground", source: "ambientcg-ice001", surface: "opaque", tileMetres: 1.8, response: { metalness: 0, roughness: 0.18 } },
|
|
65
|
+
{ name: "ground/snow", title: "Snow", group: "ground", source: "ambientcg-snow006", surface: "opaque", tileMetres: 2.4, response: { metalness: 0, roughness: 0.72 } },
|
|
66
|
+
// Fabric, plastic and stone fittings.
|
|
67
|
+
{ name: "fabric/canvas", title: "Canvas", group: "fabric", source: "ambientcg-fabric007", surface: "opaque", tileMetres: 1, response: { ...MATTE } },
|
|
68
|
+
{ name: "fabric/weave", title: "Woven fabric", group: "fabric", source: "ambientcg-fabric009", surface: "opaque", tileMetres: 0.8, response: { metalness: 0, roughness: 0.82, sheen: 0.3, sheenColor: "#ffffff", sheenRoughness: 0.8 } },
|
|
69
|
+
{ name: "plastic/matte", title: "Matte plastic", group: "fabric", source: "ambientcg-plastic006", surface: "opaque", tileMetres: 0.8, response: { metalness: 0, roughness: 0.5 } },
|
|
70
|
+
{ name: "stone/marble", title: "Marble", group: "stone", source: "ambientcg-marble001", surface: "opaque", tileMetres: 1.8, response: { metalness: 0, roughness: 0.22 } },
|
|
71
|
+
{ name: "stone/terrazzo", title: "Terrazzo", group: "stone", source: "ambientcg-terrazzo005", surface: "opaque", tileMetres: 1.6, response: { metalness: 0, roughness: 0.3 } },
|
|
72
|
+
// Glass: transmission and roughness carry the look. The smudged variant adds a scanned
|
|
73
|
+
// fingerprint normal so glass is not a mathematically clean sheet.
|
|
74
|
+
{ name: "glass/clear", title: "Clear glass", group: "glass", surface: "glass", tileMetres: 2, response: { metalness: 0, roughness: 0.03, transmission: 1, ior: 1.52, thickness: 0.01, alphaMode: "opaque" } },
|
|
75
|
+
{ name: "glass/frosted", title: "Frosted glass", group: "glass", surface: "glass", tileMetres: 2, response: { metalness: 0, roughness: 0.42, transmission: 1, ior: 1.52, thickness: 0.01, alphaMode: "opaque" } },
|
|
76
|
+
{ name: "glass/smudged", title: "Smudged glass", group: "glass", source: "ambientcg-fingerprints001", surface: "glass", tileMetres: 0.7, response: { metalness: 0, roughness: 0.12, transmission: 1, ior: 1.52, thickness: 0.01, alphaMode: "opaque" } },
|
|
77
|
+
// Water: the shell's ocean/puddle shader owns ripples; these entries carry the response and the
|
|
78
|
+
// depth tint so a game selects water by name like any other surface.
|
|
79
|
+
{ name: "water/ocean", title: "Ocean water", group: "water", surface: "water", tileMetres: 8, response: { metalness: 0.02, roughness: 0.05, alphaMode: "opaque" } },
|
|
80
|
+
{ name: "water/puddle", title: "Puddle", group: "water", surface: "water", tileMetres: 3, response: { metalness: 0, roughness: 0.06, alphaMode: "opaque" } },
|
|
81
|
+
];
|
|
82
|
+
const byName = new Map(MATERIAL_LIBRARY.map((entry) => [entry.name, entry]));
|
|
83
|
+
/** Every readable map role a library material can carry, in load order. */
|
|
84
|
+
const LIBRARY_ROLES = ["color", "normal", "roughness", "ao", "opacity"];
|
|
85
|
+
/** Resolve a library name; unknown names return `null` so a caller can report them by name. */
|
|
86
|
+
export function findLibraryMaterial(name) {
|
|
87
|
+
return byName.get(name) ?? null;
|
|
88
|
+
}
|
|
89
|
+
/** Every library name, in authored order. */
|
|
90
|
+
export function libraryMaterialNames() {
|
|
91
|
+
return MATERIAL_LIBRARY.map((entry) => entry.name);
|
|
92
|
+
}
|
|
93
|
+
/** Map roles the pinned archive of `entry` actually ships, from its catalog source declaration. */
|
|
94
|
+
export function libraryMaterialRoles(entry) {
|
|
95
|
+
if (entry.source === undefined)
|
|
96
|
+
return [];
|
|
97
|
+
const source = sourceById.get(entry.source);
|
|
98
|
+
if (source === undefined)
|
|
99
|
+
return [];
|
|
100
|
+
return LIBRARY_ROLES.filter((role) => source.materialMaps?.includes(role) === true);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Serialized runtime map URLs for a library material, matching what `assets pull <source>` writes
|
|
104
|
+
* into `<basePath>/<source-id>/`. Roles the archive does not ship are omitted, never invented.
|
|
105
|
+
*/
|
|
106
|
+
export function libraryMaterialMaps(entry, options = {}) {
|
|
107
|
+
const base = `${(options.basePath ?? "/materials").replace(/\/+$/, "")}/${entry.source ?? ""}`;
|
|
108
|
+
const source = entry.source === undefined ? undefined : sourceById.get(entry.source);
|
|
109
|
+
const textures = {};
|
|
110
|
+
for (const role of libraryMaterialRoles(entry)) {
|
|
111
|
+
const target = role === "opacity" ? "alpha" : role;
|
|
112
|
+
textures[target] = {
|
|
113
|
+
url: `${base}/${MATERIAL_MAP_FILES[role]}`,
|
|
114
|
+
colorSpace: MATERIAL_TEXTURE_SEMANTICS[target].colorSpace,
|
|
115
|
+
channel: MATERIAL_TEXTURE_SEMANTICS[target].channel,
|
|
116
|
+
physicalSize: [entry.tileMetres, entry.tileMetres],
|
|
117
|
+
...(role === "normal" ? { normalConvention: "opengl" } : {}),
|
|
118
|
+
sampler: { anisotropy: 8 },
|
|
119
|
+
...(source === undefined
|
|
120
|
+
? {}
|
|
121
|
+
: { provenance: { author: source.author, license: source.license, ...(source.homepage === undefined ? {} : { sourceUrl: source.homepage }) } }),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return textures;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Build the editable, serializable material asset for a library name: maps, response and
|
|
128
|
+
* CC0 attribution in one value the editor can save into `editor.scene.json` and the shell renders.
|
|
129
|
+
* `projection` records whether those maps should sample authored UVs or world-space triplanar.
|
|
130
|
+
*/
|
|
131
|
+
export function libraryMaterialAsset(name, options = {}) {
|
|
132
|
+
const entry = findLibraryMaterial(name);
|
|
133
|
+
if (entry === null)
|
|
134
|
+
throw new TypeError(`Unknown library material "${name}"`);
|
|
135
|
+
const source = entry.source === undefined ? undefined : sourceById.get(entry.source);
|
|
136
|
+
if (entry.source !== undefined && source === undefined)
|
|
137
|
+
throw new TypeError(`Library material "${name}" names unknown source "${entry.source}"`);
|
|
138
|
+
const textures = libraryMaterialMaps(entry, options);
|
|
139
|
+
const id = `library/${entry.name.replace(/\//g, "-")}`;
|
|
140
|
+
const asset = {
|
|
141
|
+
schemaVersion: 1,
|
|
142
|
+
id,
|
|
143
|
+
name: entry.title,
|
|
144
|
+
family: entry.surface === "glass" ? "glass" : "standard",
|
|
145
|
+
surface: { ...entry.response },
|
|
146
|
+
capabilities: materialCapabilitiesForAsset({ surface: entry.response, textures }),
|
|
147
|
+
provenance: {
|
|
148
|
+
author: source?.author ?? "ambientCG",
|
|
149
|
+
license: source?.license ?? "CC0-1.0",
|
|
150
|
+
...(source?.homepage === undefined ? {} : { sourceUrl: source.homepage }),
|
|
151
|
+
description: `ambientCG ${source === undefined ? entry.name : source.title.replace(/ \(PBR material\)$/, "")} 1K-JPG PBR maps, selected by the shared jgengine material library.`,
|
|
152
|
+
},
|
|
153
|
+
...(Object.keys(textures).length === 0 ? {} : { textures }),
|
|
154
|
+
};
|
|
155
|
+
const errors = validateMaterialAsset(asset).filter((diagnostic) => diagnostic.severity === "error");
|
|
156
|
+
if (errors.length > 0)
|
|
157
|
+
throw new TypeError(`${errors[0].path}: ${errors[0].message}`);
|
|
158
|
+
return asset;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Filter the library for a picker: case-insensitive match on name, title, group or source.
|
|
162
|
+
* Empty query returns everything, so a browser can render the full library first.
|
|
163
|
+
*/
|
|
164
|
+
export function filterLibraryMaterials(query, group) {
|
|
165
|
+
const needle = query.trim().toLowerCase();
|
|
166
|
+
return MATERIAL_LIBRARY.filter((entry) => {
|
|
167
|
+
if (group !== undefined && entry.group !== group)
|
|
168
|
+
return false;
|
|
169
|
+
if (needle.length === 0)
|
|
170
|
+
return true;
|
|
171
|
+
return `${entry.name} ${entry.title} ${entry.group} ${entry.source ?? ""}`.toLowerCase().includes(needle);
|
|
172
|
+
});
|
|
173
|
+
}
|
package/dist/cli/pull.js
CHANGED
|
@@ -4,6 +4,7 @@ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFi
|
|
|
4
4
|
import { basename, dirname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { createAssetBudgetReport } from "../assetBudget.js";
|
|
7
|
+
import { MATERIAL_LIBRARY } from "../catalogs/materialLibrary.js";
|
|
7
8
|
import { downloadPackArchive, extractGlbs, extractSpriteFiles } from "../download.js";
|
|
8
9
|
import { rankAssets } from "../find.js";
|
|
9
10
|
import { generatedIndex } from "../generated/index.js";
|
|
@@ -144,6 +145,19 @@ export async function cmdPull(argv) {
|
|
|
144
145
|
console.log(`starter: ${STARTER_SOURCE_PACKS.length} pack(s) ready`);
|
|
145
146
|
return;
|
|
146
147
|
}
|
|
148
|
+
// `library` pulls every map the shared material library pins, so one command gives a game the
|
|
149
|
+
// surfaces its names resolve to (roughly 360 MB of 1K-JPG archives for the whole set).
|
|
150
|
+
if (sourceId === "library") {
|
|
151
|
+
const pulled = new Set();
|
|
152
|
+
for (const entry of MATERIAL_LIBRARY) {
|
|
153
|
+
if (entry.source === undefined || pulled.has(entry.source))
|
|
154
|
+
continue;
|
|
155
|
+
pulled.add(entry.source);
|
|
156
|
+
await cmdPull([entry.source, ...argv.slice(1)]);
|
|
157
|
+
}
|
|
158
|
+
console.log(`library: ${pulled.size} material(s) ready`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
147
161
|
const source = sourceById.get(sourceId);
|
|
148
162
|
if (source === undefined)
|
|
149
163
|
fail(`unknown source: ${sourceId}`);
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export { generatedIndex, generatedBySource } from "./generated/index.js";
|
|
|
9
9
|
export { generatedSpriteIndex, generatedSpriteBySource } from "./generated-sprites/index.js";
|
|
10
10
|
export { readGlbDims } from "./dims.js";
|
|
11
11
|
export { buildCatalog, entryUrl, type BuildCatalogOptions } from "./catalogs/build.js";
|
|
12
|
+
export { MATERIAL_LIBRARY, filterLibraryMaterials, findLibraryMaterial, libraryMaterialAsset, libraryMaterialMaps, libraryMaterialNames, libraryMaterialRoles, type LibraryGroup, type LibraryMaterial, type LibraryMaterialOptions, type LibrarySurface, } from "./catalogs/materialLibrary.js";
|
|
12
13
|
export { classifyAssetFile, validateImportSpec, type AssetImportKind, type AssetImportSpec, type ImportSpecValidation } from "./importSpec.js";
|
|
13
14
|
export { buildSpriteCatalog, type BuildSpriteCatalogOptions } from "./catalogs/buildSprites.js";
|
|
14
15
|
export { createStarterCatalog } from "./catalogs/starter.js";
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export { generatedIndex, generatedBySource } from "./generated/index.js";
|
|
|
9
9
|
export { generatedSpriteIndex, generatedSpriteBySource } from "./generated-sprites/index.js";
|
|
10
10
|
export { readGlbDims } from "./dims.js";
|
|
11
11
|
export { buildCatalog, entryUrl } from "./catalogs/build.js";
|
|
12
|
+
export { MATERIAL_LIBRARY, filterLibraryMaterials, findLibraryMaterial, libraryMaterialAsset, libraryMaterialMaps, libraryMaterialNames, libraryMaterialRoles, } from "./catalogs/materialLibrary.js";
|
|
12
13
|
export { classifyAssetFile, validateImportSpec } from "./importSpec.js";
|
|
13
14
|
export { buildSpriteCatalog } from "./catalogs/buildSprites.js";
|
|
14
15
|
export { createStarterCatalog } from "./catalogs/starter.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized filenames a pulled material directory contains, keyed by map role. Kept in its own
|
|
3
|
+
* module so catalogs and editors can name a pulled file without loading the archive reader.
|
|
4
|
+
*/
|
|
5
|
+
export declare const MATERIAL_MAP_FILES: {
|
|
6
|
+
readonly color: "color.jpg";
|
|
7
|
+
readonly normal: "normal.jpg";
|
|
8
|
+
readonly roughness: "roughness.jpg";
|
|
9
|
+
readonly ao: "ao.jpg";
|
|
10
|
+
readonly opacity: "opacity.jpg";
|
|
11
|
+
readonly displacement: "displacement.jpg";
|
|
12
|
+
readonly ktx2: "material.ktx2";
|
|
13
|
+
};
|
|
14
|
+
/** One PBR map's role within a material, including an optional compressed KTX2 asset. */
|
|
15
|
+
export type MaterialMapRole = keyof typeof MATERIAL_MAP_FILES;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized filenames a pulled material directory contains, keyed by map role. Kept in its own
|
|
3
|
+
* module so catalogs and editors can name a pulled file without loading the archive reader.
|
|
4
|
+
*/
|
|
5
|
+
export const MATERIAL_MAP_FILES = {
|
|
6
|
+
color: "color.jpg",
|
|
7
|
+
normal: "normal.jpg",
|
|
8
|
+
roughness: "roughness.jpg",
|
|
9
|
+
ao: "ao.jpg",
|
|
10
|
+
opacity: "opacity.jpg",
|
|
11
|
+
displacement: "displacement.jpg",
|
|
12
|
+
ktx2: "material.ktx2",
|
|
13
|
+
};
|
package/dist/materials.d.ts
CHANGED
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
import type { AssetAlias } from "./manifest.js";
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
readonly color: "color.jpg";
|
|
5
|
-
readonly normal: "normal.jpg";
|
|
6
|
-
readonly roughness: "roughness.jpg";
|
|
7
|
-
readonly ao: "ao.jpg";
|
|
8
|
-
readonly displacement: "displacement.jpg";
|
|
9
|
-
readonly ktx2: "material.ktx2";
|
|
10
|
-
};
|
|
11
|
-
/** One PBR map's role within a material, including an optional compressed KTX2 asset. */
|
|
12
|
-
export type MaterialMapRole = keyof typeof MATERIAL_MAP_FILES;
|
|
2
|
+
import { type MaterialMapRole } from "./materialMapFiles.js";
|
|
3
|
+
export { MATERIAL_MAP_FILES, type MaterialMapRole } from "./materialMapFiles.js";
|
|
13
4
|
/** One normalized map pulled out of a material archive by `extractMaterialMaps`. */
|
|
14
5
|
export interface ExtractedMaterialMap {
|
|
15
6
|
/** Normalized output filename (`color.jpg`, `normal.jpg`, …). */
|
|
@@ -23,12 +14,14 @@ export interface ExtractedMaterialMap {
|
|
|
23
14
|
* URLs never depend on the provider's naming or the pulled resolution.
|
|
24
15
|
*/
|
|
25
16
|
export declare function extractMaterialMaps(archive: Uint8Array): ExtractedMaterialMap[];
|
|
26
|
-
/** URLs of one material's PBR maps; `ao`/`roughness`/`ktx2` appear only when declared by its source. */
|
|
17
|
+
/** URLs of one material's PBR maps; `ao`/`roughness`/`opacity`/`ktx2` appear only when declared by its source. */
|
|
27
18
|
export interface MaterialMaps {
|
|
28
19
|
color: string;
|
|
29
20
|
normal: string;
|
|
30
21
|
roughness?: string;
|
|
31
22
|
ao?: string;
|
|
23
|
+
/** Cut-out coverage map, present on foliage and decal-style scans. */
|
|
24
|
+
opacity?: string;
|
|
32
25
|
displacement: string;
|
|
33
26
|
ktx2?: string;
|
|
34
27
|
}
|
package/dist/materials.js
CHANGED
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
import { unzipSync } from "fflate";
|
|
2
|
+
import { MATERIAL_MAP_FILES } from "./materialMapFiles.js";
|
|
2
3
|
import { materialSources, sourceById } from "./sources/index.js";
|
|
3
|
-
|
|
4
|
-
export const MATERIAL_MAP_FILES = {
|
|
5
|
-
color: "color.jpg",
|
|
6
|
-
normal: "normal.jpg",
|
|
7
|
-
roughness: "roughness.jpg",
|
|
8
|
-
ao: "ao.jpg",
|
|
9
|
-
displacement: "displacement.jpg",
|
|
10
|
-
ktx2: "material.ktx2",
|
|
11
|
-
};
|
|
4
|
+
export { MATERIAL_MAP_FILES } from "./materialMapFiles.js";
|
|
12
5
|
const MAP_SUFFIXES = [
|
|
13
6
|
{ role: "color", pattern: /_Color\.(jpe?g|png)$/i },
|
|
14
7
|
{ role: "normal", pattern: /_NormalGL\.(jpe?g|png)$/i },
|
|
15
8
|
{ role: "roughness", pattern: /_Roughness\.(jpe?g|png)$/i },
|
|
16
9
|
{ role: "ao", pattern: /_AmbientOcclusion\.(jpe?g|png)$/i },
|
|
10
|
+
{ role: "opacity", pattern: /_Opacity\.(jpe?g|png)$/i },
|
|
17
11
|
{ role: "displacement", pattern: /_Displacement\.(jpe?g|png)$/i },
|
|
18
12
|
];
|
|
19
13
|
/**
|
|
@@ -67,6 +61,7 @@ function refFor(source, basePath) {
|
|
|
67
61
|
normal: `${base}/${MATERIAL_MAP_FILES.normal}`,
|
|
68
62
|
...(source.materialMaps?.includes("roughness") ? { roughness: `${base}/${MATERIAL_MAP_FILES.roughness}` } : {}),
|
|
69
63
|
...(source.materialMaps?.includes("ao") ? { ao: `${base}/${MATERIAL_MAP_FILES.ao}` } : {}),
|
|
64
|
+
...(source.materialMaps?.includes("opacity") ? { opacity: `${base}/${MATERIAL_MAP_FILES.opacity}` } : {}),
|
|
70
65
|
displacement: `${base}/${MATERIAL_MAP_FILES.displacement}`,
|
|
71
66
|
...(source.materialMaps?.includes("ktx2") ? { ktx2: `${base}/${MATERIAL_MAP_FILES.ktx2}` } : {}),
|
|
72
67
|
},
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { AssetSource } from "../manifest.js";
|
|
2
|
+
/** Upstream asset id pinned to a material source id, so the reverse lookup never guesses a variant. */
|
|
3
|
+
export declare const AMBIENTCG_PINNED_ASSET_IDS: ReadonlyMap<string, string>;
|
|
2
4
|
/** Upstream asset id (`Grass001`) for a material source id (`ambientcg-grass001`). */
|
|
3
5
|
export declare function ambientcgAssetId(source: AssetSource): string;
|
|
4
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Every ambientCG material source: the generated family sweep plus the pinned variants the shared
|
|
8
|
+
* library needs. A pinned id the family sweep already covers stays with the sweep, so catalog ids
|
|
9
|
+
* never appear twice.
|
|
10
|
+
*/
|
|
5
11
|
export declare const ambientcgSources: readonly AssetSource[];
|
|
@@ -40,8 +40,67 @@ const FAMILIES = [
|
|
|
40
40
|
{ family: "Paper", count: 3, categories: ["paper", "prop"], ao: [] },
|
|
41
41
|
{ family: "RoofingTiles", count: 8, categories: ["roof", "tile", "building"], ao: [6, 7, 8] },
|
|
42
42
|
];
|
|
43
|
+
const PINNED = [
|
|
44
|
+
{ assetId: "Asphalt003", categories: ["asphalt", "road", "urban"], maps: ["roughness", "ao"] },
|
|
45
|
+
{ assetId: "Asphalt021", categories: ["asphalt", "road", "urban"], maps: ["roughness", "ao"] },
|
|
46
|
+
{ assetId: "Asphalt025C", categories: ["asphalt", "road", "urban", "wet"], maps: ["roughness", "ao"] },
|
|
47
|
+
{ assetId: "Road001", categories: ["road", "asphalt", "urban"], maps: ["roughness"] },
|
|
48
|
+
{ assetId: "Road008A", categories: ["road", "asphalt", "urban", "marking"], maps: ["roughness", "ao"] },
|
|
49
|
+
{ assetId: "Concrete006", categories: ["concrete", "urban"], maps: ["roughness", "ao"] },
|
|
50
|
+
{ assetId: "Concrete013", categories: ["concrete", "urban"], maps: ["roughness", "ao"] },
|
|
51
|
+
{ assetId: "Concrete025", categories: ["concrete", "urban"], maps: ["roughness", "ao"] },
|
|
52
|
+
{ assetId: "PaintedPlaster006", categories: ["plaster", "stucco", "wall", "exterior"], maps: ["roughness", "ao"] },
|
|
53
|
+
{ assetId: "PaintedPlaster010", categories: ["plaster", "stucco", "wall", "exterior"], maps: ["roughness", "ao"] },
|
|
54
|
+
{ assetId: "Plaster001", categories: ["plaster", "stucco", "wall", "interior"], maps: ["roughness"] },
|
|
55
|
+
{ assetId: "Bricks004", categories: ["brick", "wall"], maps: ["roughness", "ao"] },
|
|
56
|
+
{ assetId: "PaintedBricks002", categories: ["brick", "wall", "painted"], maps: ["roughness", "ao"] },
|
|
57
|
+
{ assetId: "PaintedBricks005", categories: ["brick", "wall", "painted"], maps: ["roughness", "ao"] },
|
|
58
|
+
{ assetId: "Tiles007", categories: ["tile", "floor", "interior"], maps: ["roughness", "ao"] },
|
|
59
|
+
{ assetId: "PavingStones002", categories: ["paving", "stone", "path"], maps: ["roughness", "ao"] },
|
|
60
|
+
{ assetId: "PavingStones020", categories: ["paving", "stone", "path"], maps: ["roughness", "ao"] },
|
|
61
|
+
{ assetId: "RoofingTiles006", categories: ["roof", "tile", "building"], maps: ["roughness", "ao"] },
|
|
62
|
+
{ assetId: "PaintedMetal002", categories: ["metal", "painted", "prop"], maps: ["roughness", "opacity"] },
|
|
63
|
+
{ assetId: "PaintedMetal006", categories: ["metal", "painted", "prop"], maps: ["roughness", "ao"] },
|
|
64
|
+
{ assetId: "Metal007", categories: ["metal"], maps: ["roughness"] },
|
|
65
|
+
{ assetId: "Metal032", categories: ["metal", "brushed"], maps: ["roughness"] },
|
|
66
|
+
{ assetId: "CorrugatedSteel009", categories: ["metal", "corrugated", "industrial"], maps: ["roughness", "ao"] },
|
|
67
|
+
{ assetId: "DiamondPlate005A", categories: ["metal", "industrial", "floor"], maps: ["roughness", "ao"] },
|
|
68
|
+
{ assetId: "Rust007", categories: ["metal", "rust", "derelict"], maps: ["roughness", "ao"] },
|
|
69
|
+
{ assetId: "MetalPlates001", categories: ["metal", "plates", "industrial"], maps: ["roughness"] },
|
|
70
|
+
{ assetId: "Wood026", categories: ["wood", "planks"], maps: ["roughness"] },
|
|
71
|
+
{ assetId: "WoodFloor009", categories: ["wood", "floor", "interior"], maps: ["roughness", "ao"] },
|
|
72
|
+
{ assetId: "Planks003", categories: ["wood", "planks", "deck"], maps: ["roughness", "ao"] },
|
|
73
|
+
{ assetId: "PaintedWood006A", categories: ["wood", "painted", "exterior"], maps: ["roughness", "ao"] },
|
|
74
|
+
{ assetId: "WoodSiding005", categories: ["wood", "siding", "building"], maps: ["roughness", "ao"] },
|
|
75
|
+
{ assetId: "Bark002", categories: ["bark", "wood", "nature"], maps: ["roughness", "ao"] },
|
|
76
|
+
{ assetId: "Leaf001", categories: ["foliage", "leaf", "nature"], maps: ["roughness", "opacity", "ao"] },
|
|
77
|
+
{ assetId: "Leaf003", categories: ["foliage", "leaf", "nature"], maps: ["roughness", "opacity", "ao"] },
|
|
78
|
+
{ assetId: "Grass001", categories: ["ground", "grass", "nature"], maps: ["roughness", "ao"] },
|
|
79
|
+
{ assetId: "Grass004", categories: ["ground", "grass", "nature"], maps: ["roughness", "ao"] },
|
|
80
|
+
{ assetId: "Moss001", categories: ["moss", "nature"], maps: ["roughness", "ao"] },
|
|
81
|
+
{ assetId: "ScatteredLeaves008", categories: ["foliage", "leaf", "ground"], maps: ["roughness", "ao"] },
|
|
82
|
+
{ assetId: "Ground037", categories: ["ground", "dirt", "forest"], maps: ["roughness", "ao"] },
|
|
83
|
+
{ assetId: "Ground055S", categories: ["ground", "sand", "beach"], maps: ["roughness", "ao"] },
|
|
84
|
+
{ assetId: "Ground080", categories: ["ground", "sand", "beach"], maps: ["roughness", "ao"] },
|
|
85
|
+
{ assetId: "Ground095B", categories: ["ground", "mud", "wet"], maps: ["roughness", "ao"] },
|
|
86
|
+
{ assetId: "Gravel002", categories: ["ground", "gravel"], maps: ["roughness"] },
|
|
87
|
+
{ assetId: "Rock022", categories: ["rock", "stone"], maps: ["roughness", "ao"] },
|
|
88
|
+
{ assetId: "Ice001", categories: ["ice", "winter"], maps: ["roughness"] },
|
|
89
|
+
{ assetId: "Snow006", categories: ["ground", "snow", "winter"], maps: ["roughness", "ao"] },
|
|
90
|
+
{ assetId: "Fabric007", categories: ["fabric", "cloth"], maps: ["roughness", "ao"] },
|
|
91
|
+
{ assetId: "Fabric009", categories: ["fabric", "cloth", "canvas"], maps: ["roughness", "ao"] },
|
|
92
|
+
{ assetId: "Plastic006", categories: ["plastic"], maps: ["roughness"] },
|
|
93
|
+
{ assetId: "Marble001", categories: ["marble", "stone", "interior"], maps: ["roughness"] },
|
|
94
|
+
{ assetId: "Terrazzo005", categories: ["terrazzo", "floor", "interior"], maps: ["roughness"] },
|
|
95
|
+
{ assetId: "Fingerprints001", categories: ["glass", "smudge", "overlay"], maps: ["roughness", "opacity"], displacement: false },
|
|
96
|
+
];
|
|
97
|
+
/** Upstream asset id pinned to a material source id, so the reverse lookup never guesses a variant. */
|
|
98
|
+
export const AMBIENTCG_PINNED_ASSET_IDS = new Map(PINNED.map((entry) => [`ambientcg-${entry.assetId.toLowerCase()}`, entry.assetId]));
|
|
43
99
|
/** Upstream asset id (`Grass001`) for a material source id (`ambientcg-grass001`). */
|
|
44
100
|
export function ambientcgAssetId(source) {
|
|
101
|
+
const pinned = AMBIENTCG_PINNED_ASSET_IDS.get(source.id);
|
|
102
|
+
if (pinned !== undefined)
|
|
103
|
+
return pinned;
|
|
45
104
|
const suffix = source.id.replace(/^ambientcg-/, "");
|
|
46
105
|
const digits = suffix.match(/\d+$/)?.[0] ?? "";
|
|
47
106
|
const family = FAMILIES.find((entry) => entry.family.toLowerCase() === suffix.slice(0, suffix.length - digits.length));
|
|
@@ -66,5 +125,35 @@ function materialSource(family, index) {
|
|
|
66
125
|
homepage: `https://ambientcg.com/view?id=${assetId}`,
|
|
67
126
|
};
|
|
68
127
|
}
|
|
69
|
-
|
|
70
|
-
|
|
128
|
+
function pinnedSource(entry) {
|
|
129
|
+
const optional = entry.maps ?? ["roughness"];
|
|
130
|
+
return {
|
|
131
|
+
id: `ambientcg-${entry.assetId.toLowerCase()}`,
|
|
132
|
+
kind: "material",
|
|
133
|
+
provider: "ambientcg",
|
|
134
|
+
title: `${entry.assetId.replace(/(\d+)([A-Z]*)$/, " $1 $2").replace(/\s+/g, " ").trim()} (PBR material)`,
|
|
135
|
+
license: "CC0-1.0",
|
|
136
|
+
author: "ambientCG",
|
|
137
|
+
categories: entry.categories,
|
|
138
|
+
materialMaps: [
|
|
139
|
+
"color", "normal",
|
|
140
|
+
...(entry.displacement === false ? [] : ["displacement"]),
|
|
141
|
+
...(optional.includes("roughness") ? ["roughness"] : []),
|
|
142
|
+
...(optional.includes("ao") ? ["ao"] : []),
|
|
143
|
+
...(optional.includes("opacity") ? ["opacity"] : []),
|
|
144
|
+
],
|
|
145
|
+
download: { url: `https://ambientcg.com/get?file=${entry.assetId}_${RESOLUTION}.zip` },
|
|
146
|
+
homepage: `https://ambientcg.com/view?id=${entry.assetId}`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const generatedSources = FAMILIES.flatMap((family) => Array.from({ length: family.count }, (_, index) => materialSource(family, index + 1)));
|
|
150
|
+
const generatedIds = new Set(generatedSources.map((source) => source.id));
|
|
151
|
+
/**
|
|
152
|
+
* Every ambientCG material source: the generated family sweep plus the pinned variants the shared
|
|
153
|
+
* library needs. A pinned id the family sweep already covers stays with the sweep, so catalog ids
|
|
154
|
+
* never appear twice.
|
|
155
|
+
*/
|
|
156
|
+
export const ambientcgSources = [
|
|
157
|
+
...generatedSources,
|
|
158
|
+
...PINNED.filter((entry) => !generatedIds.has(`ambientcg-${entry.assetId.toLowerCase()}`)).map(pinnedSource),
|
|
159
|
+
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/assets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Self-generating, license-verified index of thousands of CC0 3D models for JGengine. Sources fetch from providers' own CDNs at pull time; the npm tarball ships only the typed index and CLI (no GLB bytes).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@gltf-transform/core": "4.2.1",
|
|
39
39
|
"@gltf-transform/extensions": "4.2.1",
|
|
40
40
|
"@gltf-transform/functions": "4.2.1",
|
|
41
|
-
"@jgengine/core": "^0.
|
|
41
|
+
"@jgengine/core": "^0.27.0",
|
|
42
42
|
"fflate": "^0.8.2"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
package/skills/jgengine/api.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
8
8
|
- `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
9
|
-
- `VERSION` (const): const VERSION: "0.
|
|
9
|
+
- `VERSION` (const): const VERSION: "0.27.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
10
10
|
|
|
11
11
|
## @jgengine/core/authoring
|
|
12
12
|
|
|
@@ -82,25 +82,42 @@
|
|
|
82
82
|
- `Devtools` (interface): interface Devtools — Top-level devtools facade aggregating frame, profile, render, logs, latency, controls, discover, overrides, and probes.
|
|
83
83
|
- `DevtoolsControl` (interface): interface DevtoolsControl — Registered tunable control with its resolved schema metadata and read/write/reset accessors.
|
|
84
84
|
- `DevtoolsControlKind` (type): type DevtoolsControlKind = | "slider" | "toggle" | "color" | "select" | "text" | "vec2" | "vec3" | "vec4" | "interval" | "angle" | "enum" — ⚠ undocumented
|
|
85
|
-
- `DevtoolsLogEntry` (interface): interface DevtoolsLogEntry — A single captured log line with timestamp, level, and formatted message.
|
|
85
|
+
- `DevtoolsLogEntry` (interface): interface DevtoolsLogEntry — A single captured log line with timestamp, level, owning system tag, and formatted message.
|
|
86
86
|
- `DevtoolsLogLevel` (type): type DevtoolsLogLevel = "log" | "info" | "warn" | "error" — Severity level for a captured devtools log entry.
|
|
87
|
+
- `DevtoolsLogs` (interface): interface DevtoolsLogs — Bounded log ring with per-system tags, a warning/error ring, and lifetime counts.
|
|
87
88
|
- `DevtoolsOverrides` (interface): interface DevtoolsOverrides { version: number; enabled: string[]; values: Record<string, unknown>; schemas?: Record<string, {kind: DevtoolsControlKind; schemaVersion?: number}> } — ⚠ undocumented
|
|
88
89
|
- `DevtoolsSnapshot` (interface): interface DevtoolsSnapshot — Serializable point-in-time snapshot of devtools state: frame, render, latency, logs, controls, and discovered fields.
|
|
89
90
|
- `DiscoveredEntry` (interface): interface DiscoveredEntry — A control auto-discovered by scanning an object/table, with its binding metadata and current reader.
|
|
90
91
|
- `DiscoverySkip` (interface): interface DiscoverySkip { readonly path: string; readonly reason: string } — ⚠ undocumented
|
|
91
92
|
- `FrameRecordSample` (interface): interface FrameRecordSample — Per-frame timing input recorded by callers: total frame time, sim time, and optional named phase durations.
|
|
92
93
|
- `FrameStats` (interface): interface FrameStats — Aggregated frame-timing statistics over the recent sampling window, including per-phase breakdown.
|
|
93
|
-
- `
|
|
94
|
+
- `FrameWindow` (interface): interface FrameWindow — Frame timings over the measured window, as the player sees them.
|
|
95
|
+
- `GlReport` (interface): interface GlReport — GPU-object creation counts. `total` is since the page loaded, the named fields since warmup.
|
|
96
|
+
- `LONG_FRAME_MS` (const): const LONG_FRAME_MS: 33.4 — The one perf record shape. `bun run perf` (CDP, out of process) and the in-page telemetry report (`window.__JG.report()`, the dev-server route, `bun run debug`) both produce exactly this object, so a budget check and a diff read either one.
|
|
94
97
|
- `LatencyStats` (interface): interface LatencyStats — Aggregated latency statistics (last/avg/min/max) over recorded latency samples.
|
|
98
|
+
- `LogBusCounts` (interface): interface LogBusCounts — Lifetime totals per level, plus how many lines the ring dropped.
|
|
99
|
+
- `LogBusEntry` (interface): interface LogBusEntry — One captured line: when, how bad, which system said it, and what it said.
|
|
100
|
+
- `LogBusSnapshot` (interface): interface LogBusSnapshot — Read-out of the log bus: newest-window entries, the warning/error ring, totals, and evictions.
|
|
95
101
|
- `LongFrameEvent` (interface): interface LongFrameEvent — Recorded spike event for a frame that exceeded the long-frame threshold, with culprit attribution.
|
|
96
102
|
- `NormalizedColor` (interface): interface NormalizedColor { readonly hex: string; readonly rgb: string; readonly alpha: number; readonly hasAlpha: boolean } — ⚠ undocumented
|
|
97
103
|
- `OVERRIDES_FORMAT_VERSION` (const): const OVERRIDES_FORMAT_VERSION: 1 — ⚠ undocumented
|
|
98
104
|
- `OverrideApplyDiagnostic` (interface): interface OverrideApplyDiagnostic { readonly id: string; readonly reason: string } — ⚠ undocumented
|
|
99
105
|
- `OverrideApplyResult` (interface): interface OverrideApplyResult — Outcome of applying a devtools overrides payload: counts of applied and skipped entries plus diagnostics.
|
|
106
|
+
- `PerfQuality` (type): type PerfQuality = "full" | "draft" — Graphics tier the record was measured at.
|
|
107
|
+
- `PerfReport` (interface): interface PerfReport — One measured window of one game: the record `bun run perf`, `window.__JG.report()` and `bun run debug` print.
|
|
100
108
|
- `PhaseStats` (interface): interface PhaseStats — Timing summary for a single named profiling phase across the sampling window.
|
|
109
|
+
- `ProfileEntry` (interface): interface ProfileEntry — One sampled stack frame reduced to a readable key and its self time.
|
|
110
|
+
- `ProfileReport` (interface): interface ProfileReport — CPU profile summary; only the CDP harness can produce one.
|
|
111
|
+
- `RenderReport` (interface): interface RenderReport — Renderer-side counts for the measured window; `null` where the source could not read them.
|
|
101
112
|
- `RenderSample` (interface): interface RenderSample — Snapshot of renderer resource counts (draw calls, triangles, geometries, textures) for a frame.
|
|
113
|
+
- `ReportLogLine` (interface): interface ReportLogLine — One log line as it appears in the report.
|
|
114
|
+
- `ReportLogs` (interface): interface ReportLogs — Bounded log read-out carried by the report: warn/error lines survive even a flood of info logs.
|
|
115
|
+
- `ReportTelemetry` (interface): interface ReportTelemetry — Who produced the report, and how much producing it cost.
|
|
102
116
|
- `ScanFieldMeta` (interface): interface ScanFieldMeta extends TunableOptions { kind?: DevtoolsControlKind } — ⚠ undocumented
|
|
103
117
|
- `ScanMeta` (type): type ScanMeta = Readonly<Record<string, ScanFieldMeta>> — ⚠ undocumented
|
|
118
|
+
- `SceneNodeSource` (type): type SceneNodeSource = "scene-graph" | "heap-object3d" | "unavailable" — Where the scene-node count came from: the live scene graph, a heap walk, or nowhere.
|
|
119
|
+
- `SystemProbe` (interface): interface SystemProbe — What one system reports about itself: totals it keeps anyway, named timings, and one state line.
|
|
120
|
+
- `SystemProbeReading` (interface): interface SystemProbeReading — One system's probe read out on demand; missing sections stay empty rather than absent.
|
|
104
121
|
- `Tunable` (interface): interface Tunable<T> — Typed handle to a registered control, exposing its live value and subscribe/set/reset operations.
|
|
105
122
|
- `TunableAccessor` (interface): interface TunableAccessor — Get/set accessor pair plus initial value used to bind a discovered field to a control.
|
|
106
123
|
- `TunableChoice` (interface): interface TunableChoice<T = unknown> { readonly value: T; readonly label?: string } — ⚠ undocumented
|
|
@@ -139,9 +156,20 @@
|
|
|
139
156
|
## @jgengine/core/devtools/frame
|
|
140
157
|
|
|
141
158
|
- `FrameModule` (interface): interface FrameModule — Frame subsystem exposing the frame-timing, profiling, and render-sample facades.
|
|
142
|
-
- `LONG_FRAME_MS` (const): const LONG_FRAME_MS: 33.4 — Frame duration in milliseconds above which a frame is recorded as a long-frame spike (~30fps budget).
|
|
143
159
|
- `createFrameModule` (const): const createFrameModule: (deps: { readProbes: () => Record<string, unknown>; }) => FrameModule — Create the frame subsystem that records frame/sim/phase timings, computes stats, and captures long-frame events.
|
|
144
160
|
|
|
161
|
+
## @jgengine/core/devtools/logBus
|
|
162
|
+
|
|
163
|
+
- `LOG_BUS_CAPACITY` (const): const LOG_BUS_CAPACITY: 200 — Default measured window for the log ring; older lines are evicted, never grown.
|
|
164
|
+
- `LOG_BUS_MESSAGE_MAX` (const): const LOG_BUS_MESSAGE_MAX: 400 — Longest kept message; a longer one is cut once, at write time.
|
|
165
|
+
- `LOG_BUS_SEVERE_CAPACITY` (const): const LOG_BUS_SEVERE_CAPACITY: 64 — Warnings and errors also land in their own ring, so an error is never pushed out by info traffic.
|
|
166
|
+
- `LogBus` (interface): interface LogBus — A fixed-capacity ring of log lines tagged by owning system. `write` reuses a preallocated slot — no array growth, no splice, no per-write object — so a hot path can call it and only the message string (already built by the caller) costs an allocation.
|
|
167
|
+
- `LogBusCounts` (interface): interface LogBusCounts — Lifetime totals per level, plus how many lines the ring dropped.
|
|
168
|
+
- `LogBusEntry` (interface): interface LogBusEntry — One captured line: when, how bad, which system said it, and what it said.
|
|
169
|
+
- `LogBusOptions` (interface): interface LogBusOptions — Tunables for the log ring: capacity, the severity ring's capacity, and the clock.
|
|
170
|
+
- `LogBusSnapshot` (interface): interface LogBusSnapshot — Read-out of the log bus: newest-window entries, the warning/error ring, totals, and evictions.
|
|
171
|
+
- `createLogBus` (function): function createLogBus(options: LogBusOptions = {}): LogBus — Create the bounded log bus the devtools log facade writes through.
|
|
172
|
+
|
|
145
173
|
## @jgengine/core/devtools/logs
|
|
146
174
|
|
|
147
175
|
- `LogsModule` (interface): interface LogsModule — Logs subsystem exposing the log-capture and latency-tracking facades.
|
|
@@ -153,6 +181,27 @@
|
|
|
153
181
|
- `OverridesModule` (interface): interface OverridesModule — Overrides subsystem exposing the export/apply facade for persisting and restoring control values.
|
|
154
182
|
- `createOverridesModule` (const): const createOverridesModule: (deps: { signal: ChangeSignal; controlRecords: Map<string, ControlRecord>; writeControl: (record: ControlRecord, raw: unknown) => boolean; discoveredRecords: Map<string, DiscoveredRecord>; enableDiscovered: (id: string) => void; }) => OverridesModule — Create the overrides subsystem that serializes changed control values and re-applies a saved overrides payload.
|
|
155
183
|
|
|
184
|
+
## @jgengine/core/devtools/report
|
|
185
|
+
|
|
186
|
+
- `FrameWindow` (interface): interface FrameWindow — Frame timings over the measured window, as the player sees them.
|
|
187
|
+
- `GlReport` (interface): interface GlReport — GPU-object creation counts. `total` is since the page loaded, the named fields since warmup.
|
|
188
|
+
- `LONG_FRAME_MS` (const): const LONG_FRAME_MS: 33.4 — The one perf record shape. `bun run perf` (CDP, out of process) and the in-page telemetry report (`window.__JG.report()`, the dev-server route, `bun run debug`) both produce exactly this object, so a budget check and a diff read either one.
|
|
189
|
+
- `PerfQuality` (type): type PerfQuality = "full" | "draft" — Graphics tier the record was measured at.
|
|
190
|
+
- `PerfReport` (interface): interface PerfReport — One measured window of one game: the record `bun run perf`, `window.__JG.report()` and `bun run debug` print.
|
|
191
|
+
- `ProfileEntry` (interface): interface ProfileEntry — One sampled stack frame reduced to a readable key and its self time.
|
|
192
|
+
- `ProfileReport` (interface): interface ProfileReport — CPU profile summary; only the CDP harness can produce one.
|
|
193
|
+
- `RenderReport` (interface): interface RenderReport — Renderer-side counts for the measured window; `null` where the source could not read them.
|
|
194
|
+
- `ReportLogLine` (interface): interface ReportLogLine — One log line as it appears in the report.
|
|
195
|
+
- `ReportLogs` (interface): interface ReportLogs — Bounded log read-out carried by the report: warn/error lines survive even a flood of info logs.
|
|
196
|
+
- `ReportTelemetry` (interface): interface ReportTelemetry — Who produced the report, and how much producing it cost.
|
|
197
|
+
- `SceneNodeSource` (type): type SceneNodeSource = "scene-graph" | "heap-object3d" | "unavailable" — Where the scene-node count came from: the live scene graph, a heap walk, or nowhere.
|
|
198
|
+
|
|
199
|
+
## @jgengine/core/devtools/reportEndpoint
|
|
200
|
+
|
|
201
|
+
- `REPORT_ENDPOINT_PATH` (const): const REPORT_ENDPOINT_PATH: "/__jgengine/report" — The dev-only route a running game pushes its in-page report to, and that `bun run debug <game>` reads back. One process: the page gathers, the dev server holds the newest record per game, the CLI fetches it. Nothing here ships in a production build — the Vite plugin mounts it under `apply: "serve"`.
|
|
202
|
+
- `ReportPushBody` (interface): interface ReportPushBody — Body the page POSTs: the game it measured and the record itself.
|
|
203
|
+
- `ReportRouteError` (interface): interface ReportRouteError — Body sent when there is nothing to return yet.
|
|
204
|
+
|
|
156
205
|
## @jgengine/core/devtools/rewriteTunables
|
|
157
206
|
|
|
158
207
|
- `TunableDelta` (interface): interface TunableDelta — One changed tunable to write back into game source: which table, key path, and new value.
|
|
@@ -203,8 +252,10 @@
|
|
|
203
252
|
|
|
204
253
|
- `Devtools` (interface): interface Devtools — Top-level devtools facade aggregating frame, profile, render, logs, latency, controls, discover, overrides, and probes.
|
|
205
254
|
- `DevtoolsControl` (interface): interface DevtoolsControl — Registered tunable control with its resolved schema metadata and read/write/reset accessors.
|
|
206
|
-
- `
|
|
255
|
+
- `DevtoolsLatency` (interface): interface DevtoolsLatency — Latency samples recorded by instrumented transport methods.
|
|
256
|
+
- `DevtoolsLogEntry` (interface): interface DevtoolsLogEntry — A single captured log line with timestamp, level, owning system tag, and formatted message.
|
|
207
257
|
- `DevtoolsLogLevel` (type): type DevtoolsLogLevel = "log" | "info" | "warn" | "error" — Severity level for a captured devtools log entry.
|
|
258
|
+
- `DevtoolsLogs` (interface): interface DevtoolsLogs — Bounded log ring with per-system tags, a warning/error ring, and lifetime counts.
|
|
208
259
|
- `DevtoolsSnapshot` (interface): interface DevtoolsSnapshot — Serializable point-in-time snapshot of devtools state: frame, render, latency, logs, controls, and discovered fields.
|
|
209
260
|
- `DiscoveredEntry` (interface): interface DiscoveredEntry — A control auto-discovered by scanning an object/table, with its binding metadata and current reader.
|
|
210
261
|
- `FrameRecordSample` (interface): interface FrameRecordSample — Per-frame timing input recorded by callers: total frame time, sim time, and optional named phase durations.
|
|
@@ -214,6 +265,8 @@
|
|
|
214
265
|
- `OverrideApplyResult` (interface): interface OverrideApplyResult — Outcome of applying a devtools overrides payload: counts of applied and skipped entries plus diagnostics.
|
|
215
266
|
- `PhaseStats` (interface): interface PhaseStats — Timing summary for a single named profiling phase across the sampling window.
|
|
216
267
|
- `RenderSample` (interface): interface RenderSample — Snapshot of renderer resource counts (draw calls, triangles, geometries, textures) for a frame.
|
|
268
|
+
- `SystemProbe` (interface): interface SystemProbe — What one system reports about itself: totals it keeps anyway, named timings, and one state line.
|
|
269
|
+
- `SystemProbeReading` (interface): interface SystemProbeReading — One system's probe read out on demand; missing sections stay empty rather than absent.
|
|
217
270
|
- `Tunable` (interface): interface Tunable<T> — Typed handle to a registered control, exposing its live value and subscribe/set/reset operations.
|
|
218
271
|
- `TunableAccessor` (interface): interface TunableAccessor — Get/set accessor pair plus initial value used to bind a discovered field to a control.
|
|
219
272
|
|
|
@@ -232,7 +285,7 @@
|
|
|
232
285
|
|
|
233
286
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
234
287
|
- `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
235
|
-
- `VERSION` (const): const VERSION: "0.
|
|
288
|
+
- `VERSION` (const): const VERSION: "0.27.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
236
289
|
|
|
237
290
|
## @jgengine/core/runtime/adapter
|
|
238
291
|
|
|
@@ -48,6 +48,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
48
48
|
|
|
49
49
|
- `defineSystem` (function) · `import { defineSystem } from "@jgengine/shell/gameKit"`
|
|
50
50
|
|
|
51
|
+
## devtools-log-bus — a bounded, per-system-tagged log ring a system writes to and a report reads
|
|
52
|
+
|
|
53
|
+
- `LogBus` (interface) · `import { LogBus } from "@jgengine/core/devtools/logBus"`
|
|
54
|
+
|
|
51
55
|
## editor-catalogs — Export typed gameplay catalogs for in-editor tuning.
|
|
52
56
|
|
|
53
57
|
- `EditorCatalogDefinition` (interface) · `import { EditorCatalogDefinition } from "@jgengine/shell/gameKit"`
|
|
@@ -54,7 +54,16 @@ After package publication and the shared consumer upgrade, delete the equivalent
|
|
|
54
54
|
|
|
55
55
|
A model resolved from a catalog string id is stamped `animation: "auto"`: the shell derives speed-driven `idle`/`walk`/`run` states plus `hit`/`death`/`attack` one-shots from the loaded GLB's own clip names via semantic clip roles (`@jgengine/core/game/clipRoles` — `classifyClip`, `defaultAnimationForClips`, exported `DEFAULT_CLIP_ROLE_TABLE` covering KayKit/Quaternius/Mixamo naming). No game-side clip strings needed; a clipless model is unaffected. An explicit `ModelConfig.animation`/`style.animation` always wins, and `animation: "none"` opts a placement back to the bind pose. Games rendering cloned scenes themselves get the same driver from `@jgengine/shell/render/useModelAnimation` instead of hand-rolling an `AnimationMixer`.
|
|
56
56
|
|
|
57
|
-
Material sources declare the map roles in their pinned archive through `AssetSource.materialMaps`; `pull` checks those declarations before writing. `buildMaterialCatalog` omits absent `ao`, `roughness`, and `ktx2` URLs. Pass the returned maps directly to model, building, or terrain seams, or explicitly supply a game-owned map. Do not construct optional map filenames or fill every material with synthetic AO. Check the actual archive when changing availability; ambientCG API map metadata covers the catalog, while byte verification covers the downloaded variants.
|
|
57
|
+
Material sources declare the map roles in their pinned archive through `AssetSource.materialMaps`; `pull` checks those declarations before writing. `buildMaterialCatalog` omits absent `ao`, `roughness`, `opacity`, and `ktx2` URLs. Pass the returned maps directly to model, building, or terrain seams, or explicitly supply a game-owned map. Do not construct optional map filenames or fill every material with synthetic AO. Check the actual archive when changing availability; ambientCG API map metadata covers the catalog, while byte verification covers the downloaded variants.
|
|
58
|
+
|
|
59
|
+
## Material library
|
|
60
|
+
|
|
61
|
+
`@jgengine/assets/catalogs/materialLibrary` is the shared palette: `MATERIAL_LIBRARY` names 56 surfaces (`asphalt/wet`, `metal/painted`, `stucco/painted`, `brick/red`, `wood/oak`, `foliage/leaf`, `glass/clear`, `water/ocean`, `ground/sand`, `concrete/smooth`, …), each with its ambientCG source, world tile size in metres and physical response. Select by name — never by source id or filename.
|
|
62
|
+
|
|
63
|
+
- `assets pull library --dir <public>` pulls every pinned map set (about 360 MB of 1K-JPG archives; each pulled material is then 5–7 MB beside its name). Pull into a game's `public/materials` and build assets with `libraryMaterialAsset(name)`; the returned `MaterialAsset` is the same editable value the editor writes into `editor.scene.json`.
|
|
64
|
+
- Clear and frosted glass, ocean and puddle carry no scan: ambientCG publishes no glass or water material, so those four are response parameters. `glass/smudged` adds ambientCG's fingerprint normal.
|
|
65
|
+
- Geometry without a `uv` attribute renders through `@jgengine/shell/render/triplanarMaterial` (`TriplanarAssetMaterial`, `useTriplanarAssetMaterial`, or `applyTriplanarProjection` on an owned material). It projects colour, normal, roughness and AO from world position at the asset's tile size, and instanced meshes project in instanced space. Use it for generated or imported world geometry; keep UV-mapped models on the ordinary path.
|
|
66
|
+
- Metals need an environment to reflect. A metal entry under a bare clear colour reads as coloured chrome — pair `metal/*` with `EnvironmentLighting` or an authored sky.
|
|
58
67
|
|
|
59
68
|
## Source rules
|
|
60
69
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
- `AssetProvenanceKind` (type): type AssetProvenanceKind = "committed" | "provisioned" | "dangling" — How an asset id's bytes reach a clean checkout: - `committed` — resolves to a self-hosted / remote URL carried in the shipped index (a {@link SingleAsset}); no pull step, available immediately. - `provisioned` — a pack {@link IndexEntry}; the id is declared but its GLB is fetched on demand by `assets pull <source>` into the consumer's served dir. - `dangling` — nothing in the index, singles, or aliases owns the id, so no provisioning step exists and it can never resolve on a clean clone.
|
|
15
15
|
- `AssetProvider` (type): type AssetProvider = | "quaternius" | "kaykit" | "polypizza" | "itch" | "ambientcg" | "gameicons" | "custom" — ⚠ undocumented
|
|
16
16
|
- `AssetReference` (interface): interface AssetReference — A logical asset id referenced by some consumer (a game, scene, or config), for validation.
|
|
17
|
-
- `AssetSource` (interface): interface AssetSource { id: string; kind?: AssetSourceKind; provider: AssetProvider; title: string; license: string; author: string; categories: readonly string[]; download: AssetDownload; homepage?: string; materialMaps?: readonly MaterialMapRole[]; mirror?: st… — ⚠ undocumented · used by `ambientcgSources`: Every ambientCG material source
|
|
17
|
+
- `AssetSource` (interface): interface AssetSource { id: string; kind?: AssetSourceKind; provider: AssetProvider; title: string; license: string; author: string; categories: readonly string[]; download: AssetDownload; homepage?: string; materialMaps?: readonly MaterialMapRole[]; mirror?: st… — ⚠ undocumented · used by `ambientcgSources`: Every ambientCG material source: the generated family sweep plus the pinned variants the shared library needs.
|
|
18
18
|
- `AssetSourceKind` (type): type AssetSourceKind = "model" | "material" | "sprite" — What a source's archive contains: GLB models (default), one PBR material's texture maps, or a pack of individual 2D sprite/icon files (SVG/PNG).
|
|
19
19
|
- `AssetSpace` (interface): interface AssetSpace — Catalog-level asset-space metadata: how a model is authored relative to the engine, owned by the catalog entry rather than re-derived per game. Every field is optional so an unmeasured asset stays valid; {@link resolveFacingRotationY}/{@link assetUnitScale}/{@link resolveAnchorOffset} supply the documented defaults.
|
|
20
20
|
- `BuildCatalogOptions` (interface): interface BuildCatalogOptions { basePath?: string; sources?: readonly string[]; includeAliases?: boolean; includeSingles?: boolean; extras?: readonly CatalogExtra[] } — ⚠ undocumented · used by `buildCatalog`: Builds a game's asset catalog from the generated CC0 pack index plus singles, extras, and aliases.
|
|
@@ -28,10 +28,15 @@
|
|
|
28
28
|
- `GlbTextureMetrics` (interface): interface GlbTextureMetrics — Dimensions and encoded bytes for one glTF image, including images referenced by texture extensions.
|
|
29
29
|
- `ImportSpecValidation` (type): type ImportSpecValidation = { ok: true; entry: AssetImportSpec } | { ok: false; reason: string } — Result of validating an import description; a successful entry is an independent JSON snapshot.
|
|
30
30
|
- `IndexEntry` (interface): interface IndexEntry — A source-owned model or sprite id, with optional offline model measurements.
|
|
31
|
-
- `
|
|
31
|
+
- `LibraryGroup` (type): type LibraryGroup = | "asphalt" | "concrete" | "stucco" | "brick" | "tile" | "metal" | "wood" | "foliage" | "ground" | "fabric" | "stone" | "glass" | "water" — Browse groups the library browser and `assets list` share.
|
|
32
|
+
- `LibraryMaterial` (interface): interface LibraryMaterial — One named material a game or the editor selects by name. `source` names the ambientCG catalog entry whose pulled maps it uses; glass and water carry response parameters only, because ambientCG publishes no scanned glass or water material and both are shader surfaces.
|
|
33
|
+
- `LibraryMaterialOptions` (interface): interface LibraryMaterialOptions — Options for {@link libraryMaterialMaps} and {@link libraryMaterialAsset}.
|
|
34
|
+
- `LibrarySurface` (type): type LibrarySurface = "opaque" | "foliage" | "glass" | "water" — How a library surface renders: an ordinary PBR surface, cut-out foliage, glass or water.
|
|
35
|
+
- `MATERIAL_LIBRARY` (const): const MATERIAL_LIBRARY: readonly LibraryMaterial[] — The shared library: 40+ ambientCG PBR materials plus the glass and water surfaces that need a shader rather than a scan. `satisfies` keeps every entry complete, so a missing tile size or response is a type error instead of a silently grey surface.
|
|
36
|
+
- `MATERIAL_MAP_FILES` (const): const MATERIAL_MAP_FILES: { readonly color: "color.jpg"; readonly normal: "normal.jpg"; readonly roughness: "roughness.jpg"; readonly ao: "ao.jpg"; readonly opacity: "opacity.jpg"; readonly displacement: "displacement.jpg"; readonly ktx2: "material.ktx2"; } — Normalized filenames a pulled material directory contains, keyed by map role. Kept in its own module so catalogs and editors can name a pulled file without loading the archive reader.
|
|
32
37
|
- `MaterialCatalog` (interface): interface MaterialCatalog — Resolves material ids and `material/…` aliases to `MaterialRef`s.
|
|
33
38
|
- `MaterialMapRole` (type): type MaterialMapRole = keyof typeof MATERIAL_MAP_FILES — One PBR map's role within a material, including an optional compressed KTX2 asset.
|
|
34
|
-
- `MaterialMaps` (interface): interface MaterialMaps — URLs of one material's PBR maps; `ao`/`roughness`/`ktx2` appear only when declared by its source.
|
|
39
|
+
- `MaterialMaps` (interface): interface MaterialMaps — URLs of one material's PBR maps; `ao`/`roughness`/`opacity`/`ktx2` appear only when declared by its source.
|
|
35
40
|
- `MaterialRef` (interface): interface MaterialRef — A resolved material: identity, attribution, and its normalized map URLs.
|
|
36
41
|
- `ModelDims` (interface): interface ModelDims — Measured horizontal footprint, footprint center, and lowest Y of a model in model space.
|
|
37
42
|
- `ModelSnippetOptions` (interface): interface ModelSnippetOptions { basePath?: string; seam?: "objectModels" | "entityModels" } — ⚠ undocumented · used by `modelWiringSnippet`: Copy-paste wiring for a pulled GLB id: resolve through the catalog, drop into a model seam.
|
|
@@ -54,7 +59,7 @@
|
|
|
54
59
|
- `ValidateAssetReferencesResult` (interface): interface ValidateAssetReferencesResult — The aggregate result of validating a set of references against the provisioning contract.
|
|
55
60
|
- `VerifyResult` (interface): interface VerifyResult { ok: boolean; errors: string[] } — ⚠ undocumented
|
|
56
61
|
- `aliases` (const): const aliases: readonly AssetAlias[] — Semantic keys → live catalog ids. Prefer these in games so re-homes only touch this table. Never point at kenney-*. Curated starter theme ids (`asset:person_casual`, `nature_tree`, …) come from {@link starterAliases}.
|
|
57
|
-
- `ambientcgSources` (const): const ambientcgSources: readonly AssetSource[] — Every ambientCG material source
|
|
62
|
+
- `ambientcgSources` (const): const ambientcgSources: readonly AssetSource[] — Every ambientCG material source: the generated family sweep plus the pinned variants the shared library needs. A pinned id the family sweep already covers stays with the sweep, so catalog ids never appear twice.
|
|
58
63
|
- `buildCatalog` (function): function buildCatalog(options: BuildCatalogOptions = {}): AssetCatalog — Builds a game's asset catalog from the generated CC0 pack index plus singles, extras, and aliases. Registration order is packs → singles → {@link BuildCatalogOptions.extras | extras} → aliases: packs and singles come first, extras override them last-writer-wins, and aliases resolve last so they can target an extra. See {@link BuildCatalogOptions} for filtering and opt-outs.
|
|
59
64
|
- `buildMaterialCatalog` (function): function buildMaterialCatalog(options: BuildMaterialCatalogOptions = {}): MaterialCatalog — A resolvable catalog over every `kind: "material"` source. Ids are source ids (`ambientcg-grass001`) plus the `material/…` aliases; every resolve returns the normalized map URLs under `basePath`, matching what `assets pull` writes into `<dir>/materials/<id>/`.
|
|
60
65
|
- `buildSpriteCatalog` (function): function buildSpriteCatalog(options: BuildSpriteCatalogOptions = {}): AssetCatalog — Resolves individual pulled sprite/icon ids (e.g. `gameicons-icons/sword`) to `{ url }`.
|
|
@@ -65,7 +70,9 @@
|
|
|
65
70
|
- `entryForSpriteFile` (function): function entryForSpriteFile(source: AssetSource, file: string): IndexEntry — Builds one sprite/icon `IndexEntry` — same shape as a model entry, minus `dims`.
|
|
66
71
|
- `entryUrl` (function): function entryUrl(basePath: string, entry: IndexEntry): string — ⚠ undocumented
|
|
67
72
|
- `extractMaterialMaps` (function): function extractMaterialMaps(archive: Uint8Array): ExtractedMaterialMap[] — Pulls the recognized PBR maps out of a material archive (ambientCG's flat `<Asset>_<Res>_<Map>.jpg` layout) and normalizes their names so resolved URLs never depend on the provider's naming or the pulled resolution.
|
|
73
|
+
- `filterLibraryMaterials` (function): function filterLibraryMaterials(query: string, group?: LibraryGroup): readonly LibraryMaterial[] — Filter the library for a picker: case-insensitive match on name, title, group or source. Empty query returns everything, so a browser can render the full library first.
|
|
68
74
|
- `findAssets` (function): function findAssets(query: string, options: FindOptions = {}): AssetMatch[] — The ranked matches for a query — models, packs, HUD components, and icons in one list.
|
|
75
|
+
- `findLibraryMaterial` (function): function findLibraryMaterial(name: string): LibraryMaterial | null — Resolve a library name; unknown names return `null` so a caller can report them by name.
|
|
69
76
|
- `gameiconsSources` (const): const gameiconsSources: readonly AssetSource[] — game-icons.net (https://game-icons.net) — ~4,000 CC BY 3.0 SVG item/ability icons from 40+ contributing artists, mirrored as one GitHub repo (github.com/game-icons/icons). `HEAD` resolves the default branch without pinning a name or commit, so this always mirrors the current set; the per-artist `license.txt` files inside the repo carry the individual credits behind this pack's single collective `author` field.
|
|
70
77
|
- `generatedBySource` (const): const generatedBySource: Record<string, IndexEntry[]> — ⚠ undocumented
|
|
71
78
|
- `generatedIndex` (const): const generatedIndex: IndexEntry[] — ⚠ undocumented
|
|
@@ -76,6 +83,10 @@
|
|
|
76
83
|
- `isScrapeDownload` (function): function isScrapeDownload(download: AssetDownload): download is ScrapeDownload — ⚠ undocumented
|
|
77
84
|
- `kaykitSources` (const): const kaykitSources: readonly AssetSource[] — ⚠ undocumented
|
|
78
85
|
- `keyFromSpriteFile` (function): function keyFromSpriteFile(file: string): string — Strips the `.svg`/`.png` extension off a pulled sprite/icon filename to get its id suffix.
|
|
86
|
+
- `libraryMaterialAsset` (function): function libraryMaterialAsset(name: string, options: LibraryMaterialOptions = {}): MaterialAsset — Build the editable, serializable material asset for a library name: maps, response and CC0 attribution in one value the editor can save into `editor.scene.json` and the shell renders. `projection` records whether those maps should sample authored UVs or world-space triplanar.
|
|
87
|
+
- `libraryMaterialMaps` (function): function libraryMaterialMaps(entry: LibraryMaterial, options: LibraryMaterialOptions = {}): Partial<Record<MaterialTextureRole, MaterialTextureMetadata>> — Serialized runtime map URLs for a library material, matching what `assets pull <source>` writes into `<basePath>/<source-id>/`. Roles the archive does not ship are omitted, never invented.
|
|
88
|
+
- `libraryMaterialNames` (function): function libraryMaterialNames(): readonly string[] — Every library name, in authored order.
|
|
89
|
+
- `libraryMaterialRoles` (function): function libraryMaterialRoles(entry: LibraryMaterial): readonly LibraryMapRole[] — Map roles the pinned archive of `entry` actually ships, from its catalog source declaration.
|
|
79
90
|
- `materialAliases` (const): const materialAliases: readonly AssetAlias[] — Semantic keys onto the ambientCG catalog, mirroring the model alias layer.
|
|
80
91
|
- `materialSources` (const): const materialSources: readonly AssetSource[] — Every `kind: "material"` source — one CC0 PBR material each, resolvable via `buildMaterialCatalog`.
|
|
81
92
|
- `materialWiringSnippet` (function): function materialWiringSnippet(id: string, basePath = "/materials"): string — Copy-paste wiring for a pulled PBR material: resolve the map URLs through the material catalog, then apply them onto terrain or a model.
|
|
@@ -122,6 +133,21 @@
|
|
|
122
133
|
- `BuildSpriteCatalogOptions` (interface): interface BuildSpriteCatalogOptions — Options for `buildSpriteCatalog`.
|
|
123
134
|
- `buildSpriteCatalog` (function): function buildSpriteCatalog(options: BuildSpriteCatalogOptions = {}): AssetCatalog — Resolves individual pulled sprite/icon ids (e.g. `gameicons-icons/sword`) to `{ url }`.
|
|
124
135
|
|
|
136
|
+
## @jgengine/assets/catalogs/materialLibrary
|
|
137
|
+
|
|
138
|
+
- `LibraryGroup` (type): type LibraryGroup = | "asphalt" | "concrete" | "stucco" | "brick" | "tile" | "metal" | "wood" | "foliage" | "ground" | "fabric" | "stone" | "glass" | "water" — Browse groups the library browser and `assets list` share.
|
|
139
|
+
- `LibraryMapRole` (type): type LibraryMapRole = "color" | "normal" | "roughness" | "ao" | "opacity" — Map roles a library scan can carry: the PBR set plus cut-out coverage.
|
|
140
|
+
- `LibraryMaterial` (interface): interface LibraryMaterial — One named material a game or the editor selects by name. `source` names the ambientCG catalog entry whose pulled maps it uses; glass and water carry response parameters only, because ambientCG publishes no scanned glass or water material and both are shader surfaces.
|
|
141
|
+
- `LibraryMaterialOptions` (interface): interface LibraryMaterialOptions — Options for {@link libraryMaterialMaps} and {@link libraryMaterialAsset}.
|
|
142
|
+
- `LibrarySurface` (type): type LibrarySurface = "opaque" | "foliage" | "glass" | "water" — How a library surface renders: an ordinary PBR surface, cut-out foliage, glass or water.
|
|
143
|
+
- `MATERIAL_LIBRARY` (const): const MATERIAL_LIBRARY: readonly LibraryMaterial[] — The shared library: 40+ ambientCG PBR materials plus the glass and water surfaces that need a shader rather than a scan. `satisfies` keeps every entry complete, so a missing tile size or response is a type error instead of a silently grey surface.
|
|
144
|
+
- `filterLibraryMaterials` (function): function filterLibraryMaterials(query: string, group?: LibraryGroup): readonly LibraryMaterial[] — Filter the library for a picker: case-insensitive match on name, title, group or source. Empty query returns everything, so a browser can render the full library first.
|
|
145
|
+
- `findLibraryMaterial` (function): function findLibraryMaterial(name: string): LibraryMaterial | null — Resolve a library name; unknown names return `null` so a caller can report them by name.
|
|
146
|
+
- `libraryMaterialAsset` (function): function libraryMaterialAsset(name: string, options: LibraryMaterialOptions = {}): MaterialAsset — Build the editable, serializable material asset for a library name: maps, response and CC0 attribution in one value the editor can save into `editor.scene.json` and the shell renders. `projection` records whether those maps should sample authored UVs or world-space triplanar.
|
|
147
|
+
- `libraryMaterialMaps` (function): function libraryMaterialMaps(entry: LibraryMaterial, options: LibraryMaterialOptions = {}): Partial<Record<MaterialTextureRole, MaterialTextureMetadata>> — Serialized runtime map URLs for a library material, matching what `assets pull <source>` writes into `<basePath>/<source-id>/`. Roles the archive does not ship are omitted, never invented.
|
|
148
|
+
- `libraryMaterialNames` (function): function libraryMaterialNames(): readonly string[] — Every library name, in authored order.
|
|
149
|
+
- `libraryMaterialRoles` (function): function libraryMaterialRoles(entry: LibraryMaterial): readonly LibraryMapRole[] — Map roles the pinned archive of `entry` actually ships, from its catalog source declaration.
|
|
150
|
+
|
|
125
151
|
## @jgengine/assets/catalogs/starter
|
|
126
152
|
|
|
127
153
|
- `createStarterCatalog` (function): function createStarterCatalog(options: BuildCatalogOptions = {}): AssetCatalog<ModelAssetRef> — Catalog restricted to the curated starter packs (people/props/nature/urban). Prefer this for scaffolds and probes so `asset:person_casual` etc. resolve without pulling the whole library. Full index: {@link buildCatalog}.
|
|
@@ -225,7 +251,7 @@
|
|
|
225
251
|
- `AssetAlias` (interface): interface AssetAlias { key: string; target: string } — ⚠ undocumented · used by `aliases` (@jgengine/assets): Semantic keys → live catalog ids.
|
|
226
252
|
- `AssetDownload` (type): type AssetDownload = PinnedDownload | ScrapeDownload — ⚠ undocumented
|
|
227
253
|
- `AssetProvider` (type): type AssetProvider = | "quaternius" | "kaykit" | "polypizza" | "itch" | "ambientcg" | "gameicons" | "custom" — ⚠ undocumented
|
|
228
|
-
- `AssetSource` (interface): interface AssetSource { id: string; kind?: AssetSourceKind; provider: AssetProvider; title: string; license: string; author: string; categories: readonly string[]; download: AssetDownload; homepage?: string; materialMaps?: readonly MaterialMapRole[]; mirror?: st… — ⚠ undocumented · used by `ambientcgSources` (@jgengine/assets): Every ambientCG material source
|
|
254
|
+
- `AssetSource` (interface): interface AssetSource { id: string; kind?: AssetSourceKind; provider: AssetProvider; title: string; license: string; author: string; categories: readonly string[]; download: AssetDownload; homepage?: string; materialMaps?: readonly MaterialMapRole[]; mirror?: st… — ⚠ undocumented · used by `ambientcgSources` (@jgengine/assets): Every ambientCG material source: the generated family sweep plus the pinned variants the shared library needs.
|
|
229
255
|
- `AssetSourceKind` (type): type AssetSourceKind = "model" | "material" | "sprite" — What a source's archive contains: GLB models (default), one PBR material's texture maps, or a pack of individual 2D sprite/icon files (SVG/PNG).
|
|
230
256
|
- `AssetSpace` (interface): interface AssetSpace — Catalog-level asset-space metadata: how a model is authored relative to the engine, owned by the catalog entry rather than re-derived per game. Every field is optional so an unmeasured asset stays valid; {@link resolveFacingRotationY}/{@link assetUnitScale}/{@link resolveAnchorOffset} supply the documented defaults.
|
|
231
257
|
- `CollisionMeshData` (interface): interface CollisionMeshData — Compact, serializable triangle collision mesh measured at asset reindex — the renderer-free source of mesh-accurate hitboxes. Positions are welded onto a 16-bit grid spanning `min`..`max` (model space) and stored base64-encoded, so an opted-in catalog asset ships its collision triangles inside the generated index without the runtime ever touching the rendered scene graph.
|
|
@@ -236,14 +262,19 @@
|
|
|
236
262
|
- `SingleAsset` (interface): interface SingleAsset { id: string; url: string; license: string; author: string; categories: readonly string[] } — ⚠ undocumented
|
|
237
263
|
- `isScrapeDownload` (function): function isScrapeDownload(download: AssetDownload): download is ScrapeDownload — ⚠ undocumented
|
|
238
264
|
|
|
265
|
+
## @jgengine/assets/materialMapFiles
|
|
266
|
+
|
|
267
|
+
- `MATERIAL_MAP_FILES` (const): const MATERIAL_MAP_FILES: { readonly color: "color.jpg"; readonly normal: "normal.jpg"; readonly roughness: "roughness.jpg"; readonly ao: "ao.jpg"; readonly opacity: "opacity.jpg"; readonly displacement: "displacement.jpg"; readonly ktx2: "material.ktx2"; } — Normalized filenames a pulled material directory contains, keyed by map role. Kept in its own module so catalogs and editors can name a pulled file without loading the archive reader.
|
|
268
|
+
- `MaterialMapRole` (type): type MaterialMapRole = keyof typeof MATERIAL_MAP_FILES — One PBR map's role within a material, including an optional compressed KTX2 asset.
|
|
269
|
+
|
|
239
270
|
## @jgengine/assets/materials
|
|
240
271
|
|
|
241
272
|
- `BuildMaterialCatalogOptions` (interface): interface BuildMaterialCatalogOptions — Options for `buildMaterialCatalog`.
|
|
242
273
|
- `ExtractedMaterialMap` (interface): interface ExtractedMaterialMap — One normalized map pulled out of a material archive by `extractMaterialMaps`.
|
|
243
|
-
- `MATERIAL_MAP_FILES` (const): const MATERIAL_MAP_FILES: { readonly color: "color.jpg"; readonly normal: "normal.jpg"; readonly roughness: "roughness.jpg"; readonly ao: "ao.jpg"; readonly displacement: "displacement.jpg"; readonly ktx2: "material.ktx2"; } — Normalized filenames a pulled material directory contains, keyed by map role.
|
|
274
|
+
- `MATERIAL_MAP_FILES` (const): const MATERIAL_MAP_FILES: { readonly color: "color.jpg"; readonly normal: "normal.jpg"; readonly roughness: "roughness.jpg"; readonly ao: "ao.jpg"; readonly opacity: "opacity.jpg"; readonly displacement: "displacement.jpg"; readonly ktx2: "material.ktx2"; } — Normalized filenames a pulled material directory contains, keyed by map role. Kept in its own module so catalogs and editors can name a pulled file without loading the archive reader.
|
|
244
275
|
- `MaterialCatalog` (interface): interface MaterialCatalog — Resolves material ids and `material/…` aliases to `MaterialRef`s.
|
|
245
276
|
- `MaterialMapRole` (type): type MaterialMapRole = keyof typeof MATERIAL_MAP_FILES — One PBR map's role within a material, including an optional compressed KTX2 asset.
|
|
246
|
-
- `MaterialMaps` (interface): interface MaterialMaps — URLs of one material's PBR maps; `ao`/`roughness`/`ktx2` appear only when declared by its source.
|
|
277
|
+
- `MaterialMaps` (interface): interface MaterialMaps — URLs of one material's PBR maps; `ao`/`roughness`/`opacity`/`ktx2` appear only when declared by its source.
|
|
247
278
|
- `MaterialRef` (interface): interface MaterialRef — A resolved material: identity, attribution, and its normalized map URLs.
|
|
248
279
|
- `buildMaterialCatalog` (function): function buildMaterialCatalog(options: BuildMaterialCatalogOptions = {}): MaterialCatalog — A resolvable catalog over every `kind: "material"` source. Ids are source ids (`ambientcg-grass001`) plus the `material/…` aliases; every resolve returns the normalized map URLs under `basePath`, matching what `assets pull` writes into `<dir>/materials/<id>/`.
|
|
249
280
|
- `extractMaterialMaps` (function): function extractMaterialMaps(archive: Uint8Array): ExtractedMaterialMap[] — Pulls the recognized PBR maps out of a material archive (ambientCG's flat `<Asset>_<Res>_<Map>.jpg` layout) and normalizes their names so resolved URLs never depend on the provider's naming or the pulled resolution.
|
|
@@ -292,7 +323,7 @@
|
|
|
292
323
|
|
|
293
324
|
## @jgengine/assets/sources
|
|
294
325
|
|
|
295
|
-
- `ambientcgSources` (const): const ambientcgSources: readonly AssetSource[] — Every ambientCG material source
|
|
326
|
+
- `ambientcgSources` (const): const ambientcgSources: readonly AssetSource[] — Every ambientCG material source: the generated family sweep plus the pinned variants the shared library needs. A pinned id the family sweep already covers stays with the sweep, so catalog ids never appear twice.
|
|
296
327
|
- `gameiconsSources` (const): const gameiconsSources: readonly AssetSource[] — game-icons.net (https://game-icons.net) — ~4,000 CC BY 3.0 SVG item/ability icons from 40+ contributing artists, mirrored as one GitHub repo (github.com/game-icons/icons). `HEAD` resolves the default branch without pinning a name or commit, so this always mirrors the current set; the per-artist `license.txt` files inside the repo carry the individual credits behind this pack's single collective `author` field.
|
|
297
328
|
- `kaykitSources` (const): const kaykitSources: readonly AssetSource[] — ⚠ undocumented
|
|
298
329
|
- `materialSources` (const): const materialSources: readonly AssetSource[] — Every `kind: "material"` source — one CC0 PBR material each, resolvable via `buildMaterialCatalog`.
|
|
@@ -304,8 +335,9 @@
|
|
|
304
335
|
|
|
305
336
|
## @jgengine/assets/sources/ambientcg
|
|
306
337
|
|
|
338
|
+
- `AMBIENTCG_PINNED_ASSET_IDS` (const): const AMBIENTCG_PINNED_ASSET_IDS: ReadonlyMap<string, string> — Upstream asset id pinned to a material source id, so the reverse lookup never guesses a variant.
|
|
307
339
|
- `ambientcgAssetId` (function): function ambientcgAssetId(source: AssetSource): string — Upstream asset id (`Grass001`) for a material source id (`ambientcg-grass001`).
|
|
308
|
-
- `ambientcgSources` (const): const ambientcgSources: readonly AssetSource[] — Every ambientCG material source
|
|
340
|
+
- `ambientcgSources` (const): const ambientcgSources: readonly AssetSource[] — Every ambientCG material source: the generated family sweep plus the pinned variants the shared library needs. A pinned id the family sweep already covers stays with the sweep, so catalog ids never appear twice.
|
|
309
341
|
|
|
310
342
|
## @jgengine/assets/sources/gameicons
|
|
311
343
|
|