3draft 26.11.4010 → 26.11.5001
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/APP.md +17 -8
- package/README.md +59 -2
- package/dist/gpu.d.ts +28 -3
- package/dist/gpu.js +121 -32
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/material.d.ts +4 -0
- package/dist/material.js +2 -1
- package/package.json +1 -1
package/APP.md
CHANGED
|
@@ -10,7 +10,7 @@ It is one file, `app/index.html`, plus a dependency-free static server (`app/ser
|
|
|
10
10
|
|
|
11
11
|
## The view
|
|
12
12
|
|
|
13
|
-
- **Starting view: WebGPU.** If WebGPU is unavailable, or the GPU device is lost, the app falls back to SVG by itself and says so in the footer (`G` or the `WebGPU` button switches by hand). SVG shows colour and transparency only; WebGPU also shows metal, roughness and emission. The **filmic look** (`C`) adds linear light, Fresnel and tone mapping, so highlights roll off softly and `lamp`-like materials glow; switch it off for the plain flat look.
|
|
13
|
+
- **Starting view: WebGPU.** If WebGPU is unavailable, or the GPU device is lost, the app falls back to SVG by itself and says so in the footer (`G` or the `WebGPU` button switches by hand). SVG shows colour and transparency only; WebGPU also shows metal, roughness and emission. The **filmic look** (`C`) adds linear light, Fresnel and tone mapping, so highlights roll off softly and `lamp`-like materials glow; switch it off for the plain flat look. **Smooth** (`N`) shades faces that meet at less than 40 degrees as one round surface (box corners stay sharp), and **Studio** (`K`) lights the model with a key, a fill and a rim light that follow the camera; both are on at the start (WebGPU view only). The palette's `soft` slider gives a material skin-like light: it wraps round the form, with a warm band where light turns to shade.
|
|
14
14
|
- **The drawing fills the stage.** On a desktop it covers the whole area; on a phone it is a little larger than the short side.
|
|
15
15
|
- **Drag** rotates, **double click** resets the view, the arrow keys rotate too.
|
|
16
16
|
- **The cross** follows the pointer and lies on the surface under it. The cursor is always a cross. On a phone, with no mouse, it aims at the middle.
|
|
@@ -22,10 +22,10 @@ It is one file, `app/index.html`, plus a dependency-free static server (`app/ser
|
|
|
22
22
|
| --- | --- |
|
|
23
23
|
| `w a s d r f` | fly: forward, left, back, right, up, down. Every press is a speed step; the opposite key slows down |
|
|
24
24
|
| `Space` | stop moving (all speeds to zero) |
|
|
25
|
-
| `T` |
|
|
25
|
+
| `T` | target the object under the cross (see Target mode); with no target, `T` over empty space targets the nearest object. `Tab` next target (the first one when none), `Esc` let go |
|
|
26
26
|
| `/` | show only the object under the cross; again: the whole scene, and the camera flies back to where it was |
|
|
27
27
|
| `M` | place a prism on the surface under the cross, or shoot one into empty space. **In isolated mode (`/`) it fires a bullet** instead: a prism that starts at the camera point and flies along the ray through the cursor, so its long axis is always the view direction and you always see its back; if it hits the isolated object it sinks half its length and carves that part out (A − B, the cut is made of the selected material, `U` undoes it); if it misses it flies on to 4 times the camera-to-model distance and vanishes. Only the closed parts of the model are carved; open parts are ignored (see the health check below). A carve never leaves a crack: if the result would not be a closed solid it is retried with a hair's shift, and if that fails too the model stays as it was (the note "left as it was" shows). |
|
|
28
|
-
| `
|
|
28
|
+
| `Q` | edit mode on the object under the cross / off (see below) |
|
|
29
29
|
| `V` | Spider bar (in edit mode: draw a line): the first `V` remembers a surface point, the second joins the two with a thin prism (a quarter of the base of a placed one) made of the selected material. `V` over empty space forgets the first point |
|
|
30
30
|
| `X`, `Delete` | delete the object under the cross |
|
|
31
31
|
| `U`, `Backspace` | undo: placing, deleting, painting, booleans and moves, one step at a time |
|
|
@@ -37,6 +37,8 @@ It is one file, `app/index.html`, plus a dependency-free static server (`app/ser
|
|
|
37
37
|
| `H` | move gizmo (see below) |
|
|
38
38
|
| `E` | edges and points on / off (also the `Edges` button) |
|
|
39
39
|
| `C` | filmic look on / off (also the `Filmic` button; on at the start, WebGPU view only) |
|
|
40
|
+
| `N` | smooth shading on / off (the `Smooth` button; on at the start, WebGPU view only) |
|
|
41
|
+
| `K` | studio light (key, fill, rim; follows the camera) on / off (the `Studio` button; on at the start, WebGPU view only) |
|
|
40
42
|
| `G`, `L` | SVG / WebGPU, turn the light |
|
|
41
43
|
| `?` | show / hide these shortcuts under the view (hidden at the start; also the `Keys` button) |
|
|
42
44
|
|
|
@@ -55,9 +57,9 @@ There is one gizmo at a time, and `H` cycles through three states:
|
|
|
55
57
|
|
|
56
58
|
Drag one of the arrows to slide the object along that axis; the arrow thickens under the pointer. While dragging only the picture changes; one write goes to the database when you let go, and one `U` takes the whole drag back.
|
|
57
59
|
|
|
58
|
-
## Edit mode: drawing on a model (`
|
|
60
|
+
## Edit mode: drawing on a model (`Q`)
|
|
59
61
|
|
|
60
|
-
The idea: there is a base model, and you refine or extend it by drawing lines on it. Put the cross on an object and press `
|
|
62
|
+
The idea: there is a base model, and you refine or extend it by drawing lines on it. Put the cross on an object and press `Q`. A red bar at the top says which object is being edited. Its vertices show as small grey crosses.
|
|
61
63
|
|
|
62
64
|
- **`V` draws a line.** The first `V` takes a point, the next `V` joins it to the new one, and the chain goes on from there. A point is either an existing vertex (the pointer **snaps** to vertices; a white cross shows which) or a new vertex put on the surface under the cross. On a face it splits the face into triangles, on an edge it goes into both faces, so the surface never cracks.
|
|
63
65
|
- **Two points on one face cut it.** The line becomes a real edge of the mesh: the surface gets detail exactly where you drew.
|
|
@@ -71,7 +73,7 @@ The idea: there is a base model, and you refine or extend it by drawing lines on
|
|
|
71
73
|
- `V` goes back to drawing (and drops the selection); `Esc` does the same.
|
|
72
74
|
- **`B` turns the spider bars into lines.** The prisms made with `V` in object mode (they are objects named `bar`) become lines on the edited object: each end snaps to a vertex that is close, or is put on the surface if it is near it, or stays a free point. The bars are removed (one `U` brings them back). So a plan sketched as bars can be worked on as a wire.
|
|
73
75
|
- **`X`** removes the lines at the selected point or line (or the last line); the points stay. `U` undoes every step, one key press at a time.
|
|
74
|
-
- **`Esc`** stops the line you are drawing; then it leaves a move mode back to drawing; then it leaves edit mode (so does `
|
|
76
|
+
- **`Esc`** stops the line you are drawing; then it leaves a move mode back to drawing; then it leaves edit mode (so does `Q`). The lines are only shown in edit mode.
|
|
75
77
|
|
|
76
78
|
The lines live next to the mesh as `wire: [[a, b], ...]` (vertex pairs) in the object's document, so the AI sees them too. The geometry is in the engine (README part 1): `insertVertex`, `cutFace`, `moveVertex`, `moveVertexSet`, `fillLoop`, `addLine`, `findLoop`.
|
|
77
79
|
|
|
@@ -99,11 +101,18 @@ Twelve starting materials: plaster, sand, wood, dark wood, concrete, slate, gras
|
|
|
99
101
|
|
|
100
102
|
**Pick material (`I`, or the `pick` button):** the eyedropper. `I` takes the material of the face under the cross. The `pick` button arms it, and the next click on a surface takes it (`Esc` cancels). The palette opens and that material is selected, so you can read its sliders or paint with it right away. A face that has only a colour selects the material with that colour, or starts a new material from it.
|
|
101
103
|
|
|
102
|
-
For the selected material there are
|
|
104
|
+
For the selected material there are five sliders (`metal`, `rough`, `alpha`, `soft`, 0 to 1, and `emit`, 0 to 5) and a colour; `+ new` copies it as a new material. A painted face stores the material **name**, so editing a material changes every face made of it.
|
|
103
105
|
|
|
104
106
|
## Isolating, travelling
|
|
105
107
|
|
|
106
|
-
`/` hides everything but the object under the cross and frames it. The camera pose from before is remembered and restored when you leave.
|
|
108
|
+
`/` hides everything but the object under the cross and frames it. The camera pose from before is remembered and restored when you leave.
|
|
109
|
+
### Target mode
|
|
110
|
+
|
|
111
|
+
The HUD in the top left corner shows the object under the cross (name, distance, face count) and the target.
|
|
112
|
+
|
|
113
|
+
- `T` on an object **targets** it: the camera turns to face it and from then on moves on an imaginary sphere round it, always looking at its centre. `A`/`D` swing it sideways, `R`/`F` up and down (speed levels, like everywhere), `W`/`S` change the distance (`W` stops at the object's surface), and dragging the mouse turns it over the same sphere.
|
|
114
|
+
- `T` on another object re-targets; `T` on the target or over empty space brings the camera back onto the target. There is no travel: `W` flies there.
|
|
115
|
+
- `Tab` switches to the next object (`Shift+Tab`: previous). `Space` only sets the speed to zero. `Esc` lets go, and the camera then stays exactly where it is.
|
|
107
116
|
|
|
108
117
|
## Phone
|
|
109
118
|
|
package/README.md
CHANGED
|
@@ -56,18 +56,21 @@ The palette is data: `{ name: { color, metal, rough, alpha, emit } }`. A face po
|
|
|
56
56
|
| `metal` 0..1 | paint ... metal | reflects sky and ground, tinted; strong highlight |
|
|
57
57
|
| `rough` 0..1 | mirror ... matte | widens the highlight, blurs the reflection |
|
|
58
58
|
| `alpha` 1..0 | solid ... invisible | blended in a second pass, far to near (SVG: `fill-opacity`, and a see-through face hides nothing) |
|
|
59
|
+
| `soft` 0..1 | hard ... soft | skin-like light: it wraps round the form, and a warm scattered band shows where light turns to shade (WebGPU) |
|
|
59
60
|
| `emit` 0..5 | lit ... glowing | up to 1 the colour fades to unshaded; above 1 (with `tonemap`) it is a light source, that many times brighter |
|
|
60
61
|
|
|
61
62
|
Pass the palette to the renderers: `renderView(mesh, view, { palette })`, `gpu.render(mesh, view, { palette })` (a new object when an entry changes). SVG shows colour and alpha only.
|
|
62
63
|
|
|
63
64
|
`gpu.render(mesh, view, { tonemap: true })` switches on a filmic look: lighting in linear colour, HDR light, Fresnel on the edges, ACES tone mapping, and `emit` that really glows above white (`tonemap: { exposure, emit }` to tune; off by default, so the plain look stays the same).
|
|
64
65
|
|
|
66
|
+
`smooth: true` (or a crease angle in degrees, default 40) shades by corner normals: faces that meet at a shallow angle share their normals, so a faceted surface looks round, and a 90 degree box corner stays sharp (`cornerNormals(mesh, degrees)` is the pure helper). `studio: true` replaces the single sun with three lights that follow the camera: key (upper left), fill (right, softer) and a rim light from behind that edges the shape (`studio: { fill, rim }` to tune). Both are off by default.
|
|
67
|
+
|
|
65
68
|
### Surfaces, picking, editing
|
|
66
69
|
|
|
67
70
|
- `pick(mesh, camera, x, y)` casts a real ray: face, point, normal, distance. `ray` is the inverse of `project`; `crosshair` builds a cross lying on the surface; `standOn` returns a prism standing on a picked surface.
|
|
68
71
|
- `validate(mesh)` returns text notes: closed shell, winding, planar and convex faces. `extrudeFace`, `moveVertices`, `extrude`, `rotate`, `merge`, `bounds` shape meshes.
|
|
69
72
|
- `boolean(a, b, op)` (also `union`, `subtract`, `intersect`) is solid modelling on BSP trees: faces are split by planes, so the result stays convex and planar. Both operands must be closed with outward faces; an open shell (a wall without thickness) is refused with the reason. With `{ strict: false }` the parts that are not closed solids are left out of the calculation instead (a union keeps them as they are, a subtract keeps those of `a`); `health(mesh)` is the check behind it: the connected parts, which are closed, and how many faces a boolean can work with. Face colours and materials are kept; cut faces take the colour of the cutting solid. The result is welded and T-junctions are repaired. A solid stays a solid: a result that is not a closed, consistently wound solid (numerical trouble with coincident faces) is thrown away and the operation is repeated with the second operand nudged by a tiny amount; if no try is sound it throws and the caller keeps the original.
|
|
70
|
-
- **Drawing on a mesh** (`wire.ts`): `insertVertex(mesh, face, point)` puts a vertex on a surface (inside a face it makes a triangle fan, on an edge it joins both faces), `cutFace(mesh, a, b)` cuts a face along a line between two of its vertices, `moveVertex(mesh, i, to)` moves one (polygons that use it become triangles, so faces stay planar), `fillLoop(mesh, loop)` closes a loop of vertices into a face, `addLine` and `findLoop` keep the wire (a list of vertex pairs). These are what the viewer's edit mode (`
|
|
73
|
+
- **Drawing on a mesh** (`wire.ts`): `insertVertex(mesh, face, point)` puts a vertex on a surface (inside a face it makes a triangle fan, on an edge it joins both faces), `cutFace(mesh, a, b)` cuts a face along a line between two of its vertices, `moveVertex(mesh, i, to)` moves one (polygons that use it become triangles, so faces stay planar), `fillLoop(mesh, loop)` closes a loop of vertices into a face, `addLine` and `findLoop` keep the wire (a list of vertex pairs). These are what the viewer's edit mode (`Q`, see [APP.md](APP.md)) uses.
|
|
71
74
|
- **HEXAW kernel.** The picking hot loop is also written in [HEXAW](https://github.com/Pengeszikra/hexaw) (`kernel/pick.hexaw`, 569 bytes of WebAssembly). `initKernel()` starts it; `kernel.pick` returns exactly what `pick` returns, 0.14 ms instead of 1.7 ms on 7200 faces. The module is embedded as base64 (`src/kernel-bytes.ts`), so the engine needs neither HEXAW nor a fetch at runtime. `npm run kernel` re-assembles it.
|
|
72
75
|
|
|
73
76
|
### TypeScript and `|>`
|
|
@@ -92,6 +95,60 @@ A single `app/index.html` plus a dependency-free static server. Person and AI wo
|
|
|
92
95
|
|
|
93
96
|
**How to use it, all keys and the database layout: [APP.md](APP.md).**
|
|
94
97
|
|
|
98
|
+
### Keys
|
|
99
|
+
|
|
100
|
+
**Moving and looking**
|
|
101
|
+
|
|
102
|
+
| Key | What it does |
|
|
103
|
+
|---|---|
|
|
104
|
+
| drag | turns the view (in target mode: moves the camera over the sphere round the target) |
|
|
105
|
+
| arrow keys | turn the view in steps |
|
|
106
|
+
| `W` / `S` | forward / back: speed levels, every press is one step more or less |
|
|
107
|
+
| `A` / `D` | left / right (speed levels) |
|
|
108
|
+
| `R` / `F` | up / down (speed levels) |
|
|
109
|
+
| `Space` | speed to zero (the target stays) |
|
|
110
|
+
| `/` | show only the model under the cross; again: the whole scene |
|
|
111
|
+
|
|
112
|
+
**Target mode**
|
|
113
|
+
|
|
114
|
+
| Key | What it does |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `T` | target the object under the cross; with a target: another object re-targets, the target itself or empty space brings the camera back onto it; with no target over empty space: the nearest object |
|
|
117
|
+
| `Tab` / `Shift+Tab` | next / previous target (the first one when there is none) |
|
|
118
|
+
| `Esc` | let go; the camera stays where it is |
|
|
119
|
+
| `A D R F`, `W S`, drag | swing round the target, closer or farther (`W` stops at the surface) |
|
|
120
|
+
|
|
121
|
+
**Editing and objects**
|
|
122
|
+
|
|
123
|
+
| Key | What it does |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `Q` | edit mode on / off |
|
|
126
|
+
| `V` | spider bar between two points; in edit mode: draw a line |
|
|
127
|
+
| `Enter` | edit mode: close a loop into a face |
|
|
128
|
+
| `B` | edit mode: turn the old `V` bars into lines |
|
|
129
|
+
| `1` / `2` / `3` | edit mode: move points / lines / faces |
|
|
130
|
+
| `M` | put a prism down; in isolated mode: shoot it (it carves) |
|
|
131
|
+
| `O` | boolean: mark two objects, then pick the operation |
|
|
132
|
+
| `X` | delete the object under the cross |
|
|
133
|
+
| `H` | move gizmo: 1st world axes, 2nd own axes, 3rd off |
|
|
134
|
+
| `U` / `Backspace` | undo |
|
|
135
|
+
|
|
136
|
+
**Material and look**
|
|
137
|
+
|
|
138
|
+
| Key | What it does |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `P` | material palette (Shift+click paints a face, Shift+double click the whole object) |
|
|
141
|
+
| `I` | pick the material under the cross |
|
|
142
|
+
| `G` | SVG / WebGPU view |
|
|
143
|
+
| `C` | filmic look |
|
|
144
|
+
| `N` | smooth shading |
|
|
145
|
+
| `K` | studio light |
|
|
146
|
+
| `L` | turn the light |
|
|
147
|
+
| `E` | edges and points on / off |
|
|
148
|
+
| `?` | show / hide the key hints |
|
|
149
|
+
|
|
150
|
+
In edit mode `Esc` first stops the line being drawn, then leaves a move mode, then leaves edit mode (and keeps the target).
|
|
151
|
+
|
|
95
152
|
---
|
|
96
153
|
|
|
97
154
|
## 3. Engine and LLM
|
|
@@ -106,7 +163,7 @@ The engine is small enough to read whole (about 2000 lines) and everything it wo
|
|
|
106
163
|
|
|
107
164
|
Y is up, the ground is y = 0, faces are counter-clockwise seen from outside.
|
|
108
165
|
|
|
109
|
-
Examples: `examples/table.js`, `examples/bridge.js` (50 named objects), `examples/cessna.js` (one mesh, 9 colours), and `examples/house.js` (a house converted from Blender with `tools/blend/`, 45 objects, approximate colours).
|
|
166
|
+
Examples: `examples/table.js`, `examples/bridge.js` (50 named objects), `examples/cessna.js` (one mesh, 9 colours), and `examples/head.js` (a stylized head: 7 objects with their own palette, soft skin; try N and K), and `examples/house.js` (a house converted from Blender with `tools/blend/`, 45 objects, approximate colours).
|
|
110
167
|
|
|
111
168
|
---
|
|
112
169
|
|
package/dist/gpu.d.ts
CHANGED
|
@@ -39,6 +39,19 @@ export interface GpuRenderOptions {
|
|
|
39
39
|
exposure?: number;
|
|
40
40
|
emit?: number;
|
|
41
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* Smooth shading: faces that meet at an angle smaller than this (degrees; `true` = 40) share their normals, so a curved
|
|
44
|
+
* surface made of flat faces looks round and sharper corners stay crisp. Off by default (every face flat).
|
|
45
|
+
*/
|
|
46
|
+
smooth?: boolean | number;
|
|
47
|
+
/**
|
|
48
|
+
* Studio light: three lights that follow the camera instead of one fixed sun: a key light (upper left), a fill light
|
|
49
|
+
* (right, softer) and a rim light from behind that edges the shape. Off by default.
|
|
50
|
+
*/
|
|
51
|
+
studio?: boolean | {
|
|
52
|
+
fill?: number;
|
|
53
|
+
rim?: number;
|
|
54
|
+
};
|
|
42
55
|
}
|
|
43
56
|
/** One copy of a prototype: its position, orthonormal frame and color. */
|
|
44
57
|
export interface Instance {
|
|
@@ -64,8 +77,19 @@ export interface Gpu {
|
|
|
64
77
|
* like camera.project(): the same pixels, plus a depth between near and far.
|
|
65
78
|
*/
|
|
66
79
|
export declare const viewProjection: (cam: Camera, near?: number, far?: number) => Float32Array;
|
|
67
|
-
/**
|
|
68
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Smooth shading: a normal for every corner of every face. The faces that meet at a point (welded by position, so a
|
|
82
|
+
* crack in the indices does not break it) are averaged, weighted by area, but only those within `crease` degrees of
|
|
83
|
+
* this face: a 90 degree box corner stays sharp, a faceted fuselage becomes round.
|
|
84
|
+
*/
|
|
85
|
+
export declare const cornerNormals: (mesh: Mesh, crease: number) => Vec3[][];
|
|
86
|
+
/**
|
|
87
|
+
* Triangle list for the GPU: face normals (flat, or smoothed with `smooth` degrees), face colors and materials.
|
|
88
|
+
* Float32Array, 14 floats per vertex.
|
|
89
|
+
*/
|
|
90
|
+
export declare const buildVertices: (mesh: Mesh, palette?: Palette, { smooth }?: {
|
|
91
|
+
smooth?: number;
|
|
92
|
+
}) => Float32Array;
|
|
69
93
|
/**
|
|
70
94
|
* Per-instance data for drawInstances: one entry { at, axes: [ex, ey, ez], color } per copy of a
|
|
71
95
|
* prototype mesh (modelled around the origin). Float32Array, 15 floats per instance.
|
|
@@ -80,7 +104,8 @@ export declare const defaultMaterial: Required<GpuMaterial>;
|
|
|
80
104
|
* prototype mesh, data from buildInstances; drawn after the mesh), background [r,g,b] 0..1,
|
|
81
105
|
* light { direction, sky, ground }, material { ambient, diffuse, specular, shininess }, dpr.
|
|
82
106
|
*/
|
|
83
|
-
export declare const createGpu: (canvas: HTMLCanvasElement, { sampleCount }?: {
|
|
107
|
+
export declare const createGpu: (canvas: HTMLCanvasElement, { sampleCount, onError }?: {
|
|
84
108
|
sampleCount?: number;
|
|
109
|
+
onError?: (message: string) => void;
|
|
85
110
|
}) => Promise<Gpu>;
|
|
86
111
|
export {};
|
package/dist/gpu.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
// the cross marks and the framing all stay valid. The pure helpers (viewProjection,
|
|
4
4
|
// buildVertices) need no GPU and are unit tested; createGpu() needs a browser.
|
|
5
5
|
import { dot, norm } from './vec.js';
|
|
6
|
-
import { faceNormal } from './mesh.js';
|
|
6
|
+
import { bounds, faceNormal } from './mesh.js';
|
|
7
7
|
import { makeCamera } from './camera.js';
|
|
8
8
|
import { surfaceOf } from './material.js';
|
|
9
|
-
const FLOATS =
|
|
9
|
+
const FLOATS = 14; // position(3) normal(3) color(3) material(4: metal, rough, alpha, emit) soft(1)
|
|
10
10
|
/**
|
|
11
11
|
* Column-major 4x4 matrix (for WebGPU, z in 0..1) that maps world points exactly
|
|
12
12
|
* like camera.project(): the same pixels, plus a depth between near and far.
|
|
@@ -29,26 +29,60 @@ const rgb = (color) => {
|
|
|
29
29
|
return [0.78, 0.78, 0.78];
|
|
30
30
|
return [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16) / 255);
|
|
31
31
|
};
|
|
32
|
-
/**
|
|
33
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Smooth shading: a normal for every corner of every face. The faces that meet at a point (welded by position, so a
|
|
34
|
+
* crack in the indices does not break it) are averaged, weighted by area, but only those within `crease` degrees of
|
|
35
|
+
* this face: a 90 degree box corner stays sharp, a faceted fuselage becomes round.
|
|
36
|
+
*/
|
|
37
|
+
export const cornerNormals = (mesh, crease) => {
|
|
38
|
+
const cosC = Math.cos((crease * Math.PI) / 180);
|
|
39
|
+
const area = mesh.faces.map((f) => faceNormal(f.v.map((k) => mesh.positions[k]))); // length = area
|
|
40
|
+
const unit = area.map((n) => (Math.hypot(...n) > 0 ? norm(n) : [0, 1, 0]));
|
|
41
|
+
const q = (bounds(mesh).radius || 1) * 1e-6;
|
|
42
|
+
const key = (p) => `${Math.round(p[0] / q)},${Math.round(p[1] / q)},${Math.round(p[2] / q)}`;
|
|
43
|
+
const at = new Map();
|
|
44
|
+
mesh.faces.forEach((f, i) => f.v.forEach((k) => {
|
|
45
|
+
const id = key(mesh.positions[k]);
|
|
46
|
+
const list = at.get(id) ?? [];
|
|
47
|
+
if (list[list.length - 1] !== i)
|
|
48
|
+
list.push(i);
|
|
49
|
+
at.set(id, list);
|
|
50
|
+
}));
|
|
51
|
+
return mesh.faces.map((f, i) => f.v.map((k) => {
|
|
52
|
+
const sum = [0, 0, 0];
|
|
53
|
+
for (const j of at.get(key(mesh.positions[k])) ?? []) {
|
|
54
|
+
if (dot(unit[i], unit[j]) < cosC)
|
|
55
|
+
continue;
|
|
56
|
+
for (let c = 0; c < 3; c++)
|
|
57
|
+
sum[c] += area[j][c];
|
|
58
|
+
}
|
|
59
|
+
return Math.hypot(...sum) > 0 ? norm(sum) : unit[i];
|
|
60
|
+
}));
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Triangle list for the GPU: face normals (flat, or smoothed with `smooth` degrees), face colors and materials.
|
|
64
|
+
* Float32Array, 14 floats per vertex.
|
|
65
|
+
*/
|
|
66
|
+
export const buildVertices = (mesh, palette, { smooth = 0 } = {}) => {
|
|
34
67
|
let tris = 0;
|
|
35
68
|
for (const f of mesh.faces)
|
|
36
69
|
tris += Math.max(0, f.v.length - 2);
|
|
37
70
|
const out = new Float32Array(tris * 3 * FLOATS);
|
|
71
|
+
const corners = smooth > 0 ? cornerNormals(mesh, smooth) : null;
|
|
38
72
|
let o = 0;
|
|
39
|
-
|
|
73
|
+
mesh.faces.forEach((face, fi) => {
|
|
40
74
|
const pts = face.v.map((k) => mesh.positions[k]);
|
|
41
|
-
const
|
|
75
|
+
const flat = norm(faceNormal(pts));
|
|
42
76
|
const s = surfaceOf(face, palette);
|
|
43
77
|
const c = rgb(s.color);
|
|
44
78
|
const mat = [s.metal, s.rough, s.alpha, s.emit];
|
|
45
79
|
for (let j = 1; j < pts.length - 1; j++) {
|
|
46
|
-
for (const
|
|
47
|
-
out.set([...
|
|
80
|
+
for (const k of [0, j, j + 1]) {
|
|
81
|
+
out.set([...pts[k], ...(corners ? corners[fi][k] : flat), ...c, ...mat, s.soft], o);
|
|
48
82
|
o += FLOATS;
|
|
49
83
|
}
|
|
50
84
|
}
|
|
51
|
-
}
|
|
85
|
+
});
|
|
52
86
|
return out;
|
|
53
87
|
};
|
|
54
88
|
const INSTANCE_FLOATS = 15; // at(3) ex(3) ey(3) ez(3) color(3)
|
|
@@ -71,17 +105,20 @@ struct U {
|
|
|
71
105
|
ground : vec4<f32>,
|
|
72
106
|
eye : vec4<f32>,
|
|
73
107
|
mat : vec4<f32>, // ambient, diffuse, specular, shininess
|
|
74
|
-
fx : vec4<f32>, // filmic (0 or 1), exposure, emission gain
|
|
108
|
+
fx : vec4<f32>, // filmic (0 or 1), exposure, emission gain, studio (0 or 1)
|
|
109
|
+
fill : vec4<f32>, // studio: xyz direction towards the fill light, w intensity
|
|
110
|
+
rim : vec4<f32>, // studio: xyz direction towards the rim light, w intensity
|
|
75
111
|
};
|
|
76
112
|
@group(0) @binding(0) var<uniform> u : U;
|
|
77
113
|
|
|
78
|
-
struct VIn { @location(0) pos : vec3<f32>, @location(1) nrm : vec3<f32>, @location(2) col : vec3<f32>, @location(8) mat : vec4<f32
|
|
114
|
+
struct VIn { @location(0) pos : vec3<f32>, @location(1) nrm : vec3<f32>, @location(2) col : vec3<f32>, @location(8) mat : vec4<f32>, @location(9) soft : f32 };
|
|
79
115
|
struct VOut {
|
|
80
116
|
@builtin(position) p : vec4<f32>,
|
|
81
117
|
@location(0) n : vec3<f32>,
|
|
82
118
|
@location(1) c : vec3<f32>,
|
|
83
119
|
@location(2) w : vec3<f32>,
|
|
84
120
|
@location(3) m : vec4<f32>, // metal, rough, alpha, emit
|
|
121
|
+
@location(4) s : f32, // soft
|
|
85
122
|
};
|
|
86
123
|
|
|
87
124
|
@vertex fn vs(i : VIn) -> VOut {
|
|
@@ -91,6 +128,7 @@ struct VOut {
|
|
|
91
128
|
o.c = i.col;
|
|
92
129
|
o.w = i.pos;
|
|
93
130
|
o.m = i.mat;
|
|
131
|
+
o.s = i.soft;
|
|
94
132
|
return o;
|
|
95
133
|
}
|
|
96
134
|
|
|
@@ -110,6 +148,7 @@ struct IIn {
|
|
|
110
148
|
o.c = i.col;
|
|
111
149
|
o.w = w;
|
|
112
150
|
o.m = vec4<f32>(0.0, 0.7, 1.0, 0.0);
|
|
151
|
+
o.s = 0.0;
|
|
113
152
|
return o;
|
|
114
153
|
}
|
|
115
154
|
|
|
@@ -124,16 +163,39 @@ fn aces(x : vec3<f32>) -> vec3<f32> {
|
|
|
124
163
|
return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), vec3<f32>(0.0), vec3<f32>(1.0));
|
|
125
164
|
}
|
|
126
165
|
|
|
166
|
+
// Direct light, shared by both looks: x diffuse, y highlight, z scattered light (soft materials).
|
|
167
|
+
// Studio: the key light is u.light (placed by the camera), plus a softer fill and a rim light that only edges the shape.
|
|
168
|
+
fn lights(n : vec3<f32>, v : vec3<f32>, shin : f32, soft : f32) -> vec3<f32> {
|
|
169
|
+
let key = normalize(u.light.xyz);
|
|
170
|
+
let ndl = dot(n, key);
|
|
171
|
+
// soft: the light wraps round the form, and a warm band of scattered light sits where light turns to shade
|
|
172
|
+
var diff = max((ndl + soft) / (1.0 + soft), 0.0);
|
|
173
|
+
let h = normalize(key + v);
|
|
174
|
+
var spec = pow(max(dot(n, h), 0.0), shin) * step(0.0, ndl);
|
|
175
|
+
let scat = soft * exp(-ndl * ndl * 5.0);
|
|
176
|
+
if (u.fx.w > 0.5) {
|
|
177
|
+
diff = diff * 0.85; // three lights add up: the key takes a little less
|
|
178
|
+
let fl = normalize(u.fill.xyz);
|
|
179
|
+
diff = diff + max((dot(n, fl) + soft) / (1.0 + soft), 0.0) * u.fill.w;
|
|
180
|
+
let rl = normalize(u.rim.xyz);
|
|
181
|
+
let edge = pow(1.0 - max(dot(n, v), 0.0), 2.5);
|
|
182
|
+
diff = diff + max(dot(n, rl), 0.0) * edge * u.rim.w * 2.0;
|
|
183
|
+
let rh = normalize(rl + v);
|
|
184
|
+
spec = spec + pow(max(dot(n, rh), 0.0), shin) * step(0.0, dot(n, rl)) * u.rim.w * 0.6 * edge;
|
|
185
|
+
}
|
|
186
|
+
return vec3<f32>(diff, spec, scat);
|
|
187
|
+
}
|
|
188
|
+
|
|
127
189
|
// The filmic path: the same light model, but in linear colour with HDR light and emission, then tone mapped.
|
|
128
190
|
fn filmic(i : VOut, n : vec3<f32>, l : vec3<f32>, v : vec3<f32>) -> vec4<f32> {
|
|
129
191
|
let metal = i.m.x;
|
|
130
192
|
let rough = i.m.y;
|
|
131
193
|
let albedo = toLinear(i.c);
|
|
132
|
-
let hemi = mix(toLinear(u.ground.rgb), toLinear(u.sky.rgb), n.y * 0.5 + 0.5) * u.mat.x;
|
|
133
|
-
let diff = max(dot(n, l), 0.0) * u.mat.y * 1.6;
|
|
134
|
-
let h = normalize(l + v);
|
|
194
|
+
let hemi = mix(toLinear(u.ground.rgb), toLinear(u.sky.rgb), n.y * 0.5 + 0.5) * u.mat.x * select(1.0, 0.7, u.fx.w > 0.5);
|
|
135
195
|
let shin = mix(u.mat.w * 7.0, u.mat.w * 0.15, rough);
|
|
136
|
-
let
|
|
196
|
+
let lt = lights(n, v, shin, i.s);
|
|
197
|
+
let diff = lt.x * u.mat.y * 1.6;
|
|
198
|
+
let spec = lt.y * mix(u.mat.z, 1.0, metal) * mix(1.0, 0.35, rough) * 2.5;
|
|
137
199
|
let r = reflect(-v, n);
|
|
138
200
|
// Schlick: edges mirror more, metal keeps its tint
|
|
139
201
|
let f0 = mix(vec3<f32>(0.04), albedo, metal);
|
|
@@ -144,6 +206,7 @@ fn filmic(i : VOut, n : vec3<f32>, l : vec3<f32>, v : vec3<f32>) -> vec4<f32> {
|
|
|
144
206
|
+ env * fres * (1.0 - rough) * 0.5
|
|
145
207
|
+ vec3<f32>(spec) * mix(vec3<f32>(1.0), albedo, metal);
|
|
146
208
|
// emission adds light, so a lamp can go above 1.0 and still keep its colour after tone mapping
|
|
209
|
+
lit = lit + albedo * vec3<f32>(1.0, 0.38, 0.26) * lt.z * 0.5 * (1.0 - metal); // soft: the warm scattered band
|
|
147
210
|
lit = lit + albedo * i.m.w * u.fx.z;
|
|
148
211
|
return vec4<f32>(toSrgb(aces(lit * u.fx.y)), i.m.z);
|
|
149
212
|
}
|
|
@@ -156,18 +219,19 @@ fn filmic(i : VOut, n : vec3<f32>, l : vec3<f32>, v : vec3<f32>) -> vec4<f32> {
|
|
|
156
219
|
if (u.fx.x > 0.5) { return filmic(i, n, l, v); }
|
|
157
220
|
let metal = i.m.x;
|
|
158
221
|
let rough = i.m.y;
|
|
159
|
-
let hemi = mix(u.ground.rgb, u.sky.rgb, n.y * 0.5 + 0.5) * u.mat.x;
|
|
160
|
-
let diff = max(dot(n, l), 0.0) * u.mat.y;
|
|
161
|
-
let h = normalize(l + v);
|
|
222
|
+
let hemi = mix(u.ground.rgb, u.sky.rgb, n.y * 0.5 + 0.5) * u.mat.x * select(1.0, 0.7, u.fx.w > 0.5);
|
|
162
223
|
// roughness widens the highlight; metal makes it strong and tinted by the color
|
|
163
224
|
let shin = mix(u.mat.w * 7.0, u.mat.w * 0.15, rough);
|
|
164
|
-
let
|
|
225
|
+
let lt = lights(n, v, shin, i.s);
|
|
226
|
+
let diff = lt.x * u.mat.y;
|
|
227
|
+
let spec = lt.y * mix(u.mat.z, 1.0, metal) * mix(1.0, 0.35, rough);
|
|
165
228
|
// metal mirrors the sky above and the ground below, blurred by roughness
|
|
166
229
|
let r = reflect(-v, n);
|
|
167
230
|
let env = mix(u.ground.rgb * 1.2, u.sky.rgb * 1.5, smoothstep(-0.25 - rough * 0.5, 0.25 + rough * 0.5, r.y));
|
|
168
231
|
var lit = i.c * (hemi + vec3<f32>(diff)) * (1.0 - metal)
|
|
169
232
|
+ i.c * env * metal * (1.0 - 0.6 * rough)
|
|
170
|
-
+ vec3<f32>(spec) * mix(vec3<f32>(1.0), i.c, metal)
|
|
233
|
+
+ vec3<f32>(spec) * mix(vec3<f32>(1.0), i.c, metal)
|
|
234
|
+
+ i.c * vec3<f32>(1.0, 0.38, 0.26) * lt.z * 0.25 * (1.0 - metal);
|
|
171
235
|
lit = mix(lit, i.c * 1.15, min(i.m.w, 1.0)); // the plain look: emit 0..1 fades to flat colour, more does not add
|
|
172
236
|
return vec4<f32>(clamp(lit, vec3<f32>(0.0), vec3<f32>(1.0)), i.m.z);
|
|
173
237
|
}
|
|
@@ -179,14 +243,18 @@ fn filmic(i : VOut, n : vec3<f32>, l : vec3<f32>, v : vec3<f32>) -> vec4<f32> {
|
|
|
179
243
|
* prototype mesh, data from buildInstances; drawn after the mesh), background [r,g,b] 0..1,
|
|
180
244
|
* light { direction, sky, ground }, material { ambient, diffuse, specular, shininess }, dpr.
|
|
181
245
|
*/
|
|
182
|
-
export const createGpu = async (canvas, { sampleCount = 4 } = {}) => {
|
|
246
|
+
export const createGpu = async (canvas, { sampleCount = 4, onError } = {}) => {
|
|
183
247
|
if (!globalThis.navigator?.gpu)
|
|
184
248
|
throw new Error('WebGPU is not available in this browser');
|
|
185
249
|
const adapter = await navigator.gpu.requestAdapter();
|
|
186
250
|
if (!adapter)
|
|
187
251
|
throw new Error('No WebGPU adapter found');
|
|
188
252
|
const device = await adapter.requestDevice();
|
|
189
|
-
device.addEventListener('uncapturederror', (e) =>
|
|
253
|
+
device.addEventListener('uncapturederror', (e) => {
|
|
254
|
+
const message = String(e.error?.message ?? e);
|
|
255
|
+
console.error('WebGPU:', message);
|
|
256
|
+
onError?.(message);
|
|
257
|
+
});
|
|
190
258
|
device.lost.then((info) => console.error('WebGPU device lost:', info.reason, info.message));
|
|
191
259
|
const context = canvas.getContext('webgpu');
|
|
192
260
|
if (!context)
|
|
@@ -194,6 +262,12 @@ export const createGpu = async (canvas, { sampleCount = 4 } = {}) => {
|
|
|
194
262
|
const format = navigator.gpu.getPreferredCanvasFormat();
|
|
195
263
|
context.configure({ device, format, alphaMode: 'opaque', });
|
|
196
264
|
const module = device.createShaderModule({ code: SHADER });
|
|
265
|
+
// shader compile errors are reported to onError too (the browser only logs them to the console)
|
|
266
|
+
module.getCompilationInfo?.().then((info) => {
|
|
267
|
+
for (const m of info.messages ?? [])
|
|
268
|
+
if (m.type === 'error')
|
|
269
|
+
onError?.(`shader line ${m.lineNum}: ${m.message}`);
|
|
270
|
+
});
|
|
197
271
|
const meshLayout = {
|
|
198
272
|
arrayStride: FLOATS * 4,
|
|
199
273
|
attributes: [
|
|
@@ -201,6 +275,7 @@ export const createGpu = async (canvas, { sampleCount = 4 } = {}) => {
|
|
|
201
275
|
{ shaderLocation: 1, offset: 12, format: 'float32x3' },
|
|
202
276
|
{ shaderLocation: 2, offset: 24, format: 'float32x3' },
|
|
203
277
|
{ shaderLocation: 8, offset: 36, format: 'float32x4' },
|
|
278
|
+
{ shaderLocation: 9, offset: 52, format: 'float32' },
|
|
204
279
|
],
|
|
205
280
|
};
|
|
206
281
|
const instanceLayout = {
|
|
@@ -208,8 +283,14 @@ export const createGpu = async (canvas, { sampleCount = 4 } = {}) => {
|
|
|
208
283
|
stepMode: 'instance',
|
|
209
284
|
attributes: [3, 4, 5, 6, 7].map((loc, k) => ({ shaderLocation: loc, offset: k * 12, format: 'float32x3' })),
|
|
210
285
|
};
|
|
286
|
+
// ONE explicit layout for every pipeline: with 'auto' each pipeline gets its own, and a bind group made for one is refused by the others
|
|
287
|
+
const bindLayout = device.createBindGroupLayout({
|
|
288
|
+
// visibility 1 | 2 = GPUShaderStage.VERTEX | FRAGMENT
|
|
289
|
+
entries: [{ binding: 0, visibility: 1 | 2, buffer: { type: 'uniform' } }],
|
|
290
|
+
});
|
|
291
|
+
const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [bindLayout] });
|
|
211
292
|
const make = (entry, buffers, glass = false) => device.createRenderPipeline({
|
|
212
|
-
layout:
|
|
293
|
+
layout: pipelineLayout,
|
|
213
294
|
vertex: { module, entryPoint: entry, buffers },
|
|
214
295
|
fragment: {
|
|
215
296
|
module,
|
|
@@ -235,12 +316,13 @@ export const createGpu = async (canvas, { sampleCount = 4 } = {}) => {
|
|
|
235
316
|
const glassPipeline = make('vs', [meshLayout], true);
|
|
236
317
|
// the instanced pipeline reads position and normal only from the prototype (locations 0, 1)
|
|
237
318
|
const instPipeline = make('vsi', [{ ...meshLayout, attributes: meshLayout.attributes.slice(0, 2) }, instanceLayout]);
|
|
238
|
-
const uniforms = device.createBuffer({ size:
|
|
239
|
-
const bind = device.createBindGroup({ layout:
|
|
240
|
-
const instBind = device.createBindGroup({ layout:
|
|
319
|
+
const uniforms = device.createBuffer({ size: 192, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
320
|
+
const bind = device.createBindGroup({ layout: bindLayout, entries: [{ binding: 0, resource: { buffer: uniforms } }] });
|
|
321
|
+
const instBind = device.createBindGroup({ layout: bindLayout, entries: [{ binding: 0, resource: { buffer: uniforms } }] });
|
|
241
322
|
// Geometry that did not change since the last frame is not rebuilt or uploaded again.
|
|
242
323
|
let uploaded = null; // the mesh now in vertexBuffer
|
|
243
324
|
let uploadedPalette; // and the palette it was built with
|
|
325
|
+
let uploadedSmooth = 0; // and the smoothing
|
|
244
326
|
let vertexCount = 0;
|
|
245
327
|
let protoMesh = null;
|
|
246
328
|
let protoBuffer = null;
|
|
@@ -271,14 +353,16 @@ export const createGpu = async (canvas, { sampleCount = 4 } = {}) => {
|
|
|
271
353
|
depth = device.createTexture({ size: [n, n], format: 'depth24plus', sampleCount, usage: GPUTextureUsage.RENDER_ATTACHMENT });
|
|
272
354
|
color = sampleCount > 1 ? device.createTexture({ size: [n, n], format, sampleCount, usage: GPUTextureUsage.RENDER_ATTACHMENT }) : null;
|
|
273
355
|
};
|
|
274
|
-
const draw = (mesh, view, { size = 400, fit, background = [0.98, 0.98, 0.97], light = {}, material = {}, palette, dpr = globalThis.devicePixelRatio || 1, instances, tonemap = false } = {}, readback = false) => {
|
|
356
|
+
const draw = (mesh, view, { size = 400, fit, background = [0.98, 0.98, 0.97], light = {}, material = {}, palette, dpr = globalThis.devicePixelRatio || 1, instances, tonemap = false, smooth = false, studio = false } = {}, readback = false) => {
|
|
275
357
|
const L = { ...defaultLight, ...light };
|
|
276
358
|
const filmic = tonemap ? { exposure: 1, emit: 2, ...(tonemap === true ? {} : tonemap) } : null;
|
|
277
359
|
const M = { ...defaultMaterial, ...material };
|
|
278
360
|
const cam = makeCamera(view, mesh, size, fit);
|
|
279
361
|
targets(Math.max(1, Math.round(size * dpr)));
|
|
280
|
-
|
|
281
|
-
|
|
362
|
+
const crease = smooth === true ? 40 : smooth || 0;
|
|
363
|
+
if (mesh !== uploaded || palette !== uploadedPalette || crease !== uploadedSmooth) {
|
|
364
|
+
uploadedSmooth = crease;
|
|
365
|
+
const all = buildVertices(mesh, palette, { smooth: crease });
|
|
282
366
|
// split: solid triangles are drawn first; triangles with alpha < 1 are kept apart and blended afterwards
|
|
283
367
|
const tri = 3 * FLOATS;
|
|
284
368
|
const solid = [];
|
|
@@ -346,14 +430,19 @@ export const createGpu = async (canvas, { sampleCount = 4 } = {}) => {
|
|
|
346
430
|
}
|
|
347
431
|
if (!glassTris.length)
|
|
348
432
|
glassCount = 0;
|
|
349
|
-
|
|
433
|
+
// studio rig, relative to the camera: key upper left in front, fill from the right, rim from behind and above
|
|
434
|
+
const rig = (f, r, up) => norm([0, 1, 2].map((k) => -cam.f[k] * f + cam.r[k] * r + cam.u[k] * up));
|
|
435
|
+
const S = studio ? { fill: 0.4, rim: 1, ...(studio === true ? {} : studio) } : null;
|
|
436
|
+
const u = new Float32Array(48);
|
|
350
437
|
u.set(viewProjection(cam), 0);
|
|
351
|
-
u.set([...(norm(L.direction)), 0], 16);
|
|
438
|
+
u.set([...(S ? rig(0.7, -0.5, 0.6) : norm(L.direction)), 0], 16);
|
|
352
439
|
u.set([...L.sky, 0], 20);
|
|
353
440
|
u.set([...L.ground, 0], 24);
|
|
354
441
|
u.set([...cam.eye, 0], 28);
|
|
355
442
|
u.set([M.ambient, M.diffuse, M.specular, M.shininess], 32);
|
|
356
|
-
u.set([filmic ? 1 : 0, filmic ? filmic.exposure : 1, filmic ? filmic.emit : 0, 0], 36);
|
|
443
|
+
u.set([filmic ? 1 : 0, filmic ? filmic.exposure : 1, filmic ? filmic.emit : 0, S ? 1 : 0], 36);
|
|
444
|
+
u.set([...rig(0.6, 0.8, 0.1), S ? S.fill : 0], 40);
|
|
445
|
+
u.set([...rig(-0.8, 0.3, 0.5), S ? S.rim : 0], 44);
|
|
357
446
|
device.queue.writeBuffer(uniforms, 0, u);
|
|
358
447
|
const encoder = device.createCommandEncoder();
|
|
359
448
|
const offscreen = readback
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const name: "3draft";
|
|
2
|
-
export declare const version = "26.11.
|
|
2
|
+
export declare const version = "26.11.5001";
|
|
3
3
|
export type { Vec3, Face, Mesh, Axes, ViewName, ViewSpec, Fit, Camera, Hit, Mark } from './types.js';
|
|
4
4
|
export { box, extrude, merge, rotate, bounds, faceNormal, faceCenter } from './mesh.js';
|
|
5
5
|
export type { BoxOptions } from './mesh.js';
|
|
@@ -14,7 +14,7 @@ export { standOn } from './place.js';
|
|
|
14
14
|
export type { StandOptions, Frame } from './place.js';
|
|
15
15
|
export { paintOrder } from './order.js';
|
|
16
16
|
export type { PaintItem } from './order.js';
|
|
17
|
-
export { createGpu, viewProjection, buildVertices, buildInstances, defaultLight, defaultMaterial } from './gpu.js';
|
|
17
|
+
export { createGpu, viewProjection, buildVertices, cornerNormals, buildInstances, defaultLight, defaultMaterial } from './gpu.js';
|
|
18
18
|
export type { Gpu, GpuLight, GpuMaterial, GpuRenderOptions, Instance, Pixels } from './gpu.js';
|
|
19
19
|
export { renderView, renderViews, renderSheet } from './svg.js';
|
|
20
20
|
export type { RenderOptions } from './svg.js';
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// a target cross that lies on the surface under the cursor. No textures, no
|
|
5
5
|
// shading yet.
|
|
6
6
|
export const name = '3draft';
|
|
7
|
-
export const version = '26.11.
|
|
7
|
+
export const version = '26.11.5001';
|
|
8
8
|
export { box, extrude, merge, rotate, bounds, faceNormal, faceCenter } from './mesh.js';
|
|
9
9
|
export { validate } from './check.js';
|
|
10
10
|
export { boolean, union, subtract, intersect, health } from './csg.js';
|
|
@@ -13,7 +13,7 @@ export { VIEW_ORDER, makeCamera, orbit, project, ray } from './camera.js';
|
|
|
13
13
|
export { pick, crosshair } from './pick.js';
|
|
14
14
|
export { standOn } from './place.js';
|
|
15
15
|
export { paintOrder } from './order.js';
|
|
16
|
-
export { createGpu, viewProjection, buildVertices, buildInstances, defaultLight, defaultMaterial } from './gpu.js';
|
|
16
|
+
export { createGpu, viewProjection, buildVertices, cornerNormals, buildInstances, defaultLight, defaultMaterial } from './gpu.js';
|
|
17
17
|
export { renderView, renderViews, renderSheet } from './svg.js';
|
|
18
18
|
export { surfaceOf, DEFAULTS as MATERIAL_DEFAULTS } from './material.js';
|
|
19
19
|
export { displayVersion } from './version.js';
|
package/dist/material.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface Material {
|
|
|
10
10
|
alpha?: number;
|
|
11
11
|
/** 0..5: how much the color glows on its own, unlit. Up to 1 it fades from lit to flat colour; above 1 (WebGPU `tonemap`) it is a light source: that many times brighter. */
|
|
12
12
|
emit?: number;
|
|
13
|
+
/** 0..1: soft, skin-like light. The light wraps round the form and a warm, scattered band shows where light turns to shade. */
|
|
14
|
+
soft?: number;
|
|
13
15
|
}
|
|
14
16
|
export type Palette = Record<string, Material>;
|
|
15
17
|
/** What a face is made of, all numbers filled in. */
|
|
@@ -19,6 +21,7 @@ export interface Surface {
|
|
|
19
21
|
rough: number;
|
|
20
22
|
alpha: number;
|
|
21
23
|
emit: number;
|
|
24
|
+
soft: number;
|
|
22
25
|
}
|
|
23
26
|
export declare const EMIT_MAX = 5;
|
|
24
27
|
export declare const FALLBACK_COLOR = "#c8c8c8";
|
|
@@ -27,6 +30,7 @@ export declare const DEFAULTS: {
|
|
|
27
30
|
readonly rough: 0.7;
|
|
28
31
|
readonly alpha: 1;
|
|
29
32
|
readonly emit: 0;
|
|
33
|
+
readonly soft: 0;
|
|
30
34
|
};
|
|
31
35
|
/** The surface of a face: its palette entry when it names one, else just its color with default properties. */
|
|
32
36
|
export declare const surfaceOf: (face: Pick<Face, "color" | "mat">, palette?: Palette) => Surface;
|
package/dist/material.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
const clamp = (n, fallback, max = 1) => (typeof n === 'number' && Number.isFinite(n) ? Math.max(0, Math.min(max, n)) : fallback);
|
|
5
5
|
export const EMIT_MAX = 5;
|
|
6
6
|
export const FALLBACK_COLOR = '#c8c8c8';
|
|
7
|
-
export const DEFAULTS = { metal: 0, rough: 0.7, alpha: 1, emit: 0 };
|
|
7
|
+
export const DEFAULTS = { metal: 0, rough: 0.7, alpha: 1, emit: 0, soft: 0 };
|
|
8
8
|
/** The surface of a face: its palette entry when it names one, else just its color with default properties. */
|
|
9
9
|
export const surfaceOf = (face, palette) => {
|
|
10
10
|
const m = face.mat !== undefined ? palette?.[face.mat] : undefined;
|
|
@@ -14,5 +14,6 @@ export const surfaceOf = (face, palette) => {
|
|
|
14
14
|
rough: clamp(m?.rough, DEFAULTS.rough),
|
|
15
15
|
alpha: clamp(m?.alpha, DEFAULTS.alpha),
|
|
16
16
|
emit: clamp(m?.emit, DEFAULTS.emit, EMIT_MAX),
|
|
17
|
+
soft: clamp(m?.soft, DEFAULTS.soft),
|
|
17
18
|
};
|
|
18
19
|
};
|