poppygl 0.0.18 → 0.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/index.d.ts +22 -2
- package/dist/index.js +608 -267
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -53,6 +53,12 @@ const png = await renderGLTFToPNGBufferFromGLBBuffer(glb, {
|
|
|
53
53
|
- `supersampling`: render at `width * supersampling` / `height * supersampling`, then downsample (default `1`).
|
|
54
54
|
- `fov`: vertical field of view in degrees (defaults to `35`).
|
|
55
55
|
- `camPos` and `lookAt`: override the auto-framed camera position and target.
|
|
56
|
+
- `up`: choose the world-up axis for the camera with `"y+" | "y-" | "x+" | "x-" | "z+" | "z-"`.
|
|
57
|
+
- `cameraRotation`: apply Euler degrees `{ x, y, z }` to define camera orientation directly. When set, it takes precedence over `lookAt`.
|
|
58
|
+
- `debugPoints`: optional `{ label, position }[]` overlay rendered on top of the final PNG for world-space debugging markers.
|
|
59
|
+
- `debugFontSize`: optional pixel size for `debugPoints` labels; when omitted the renderer derives a size from the image dimensions.
|
|
60
|
+
- `debugPointColor`: optional RGB tuple for debug point markers.
|
|
61
|
+
- `debugLabelColor`: optional RGB tuple for debug point labels.
|
|
56
62
|
- `lightDir`: normalized directional light vector (defaults to a top-right key light).
|
|
57
63
|
- `ambient`: ambient lighting contribution (0–1, defaults to `0.2`).
|
|
58
64
|
- `gamma`: gamma correction applied to the output (defaults to `2.2`).
|
package/dist/index.d.ts
CHANGED
|
@@ -53,6 +53,20 @@ interface GLTFScene {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
declare const DEFAULT_LIGHT_DIR: readonly [-0.4, -0.9, -0.2];
|
|
56
|
+
interface DebugPoint {
|
|
57
|
+
label: string;
|
|
58
|
+
position: {
|
|
59
|
+
x: number;
|
|
60
|
+
y: number;
|
|
61
|
+
z: number;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
type CameraUp = "y+" | "y-" | "x+" | "x-" | "z+" | "z-";
|
|
65
|
+
interface CameraRotation {
|
|
66
|
+
x: number;
|
|
67
|
+
y: number;
|
|
68
|
+
z: number;
|
|
69
|
+
}
|
|
56
70
|
/**
|
|
57
71
|
* Convert a hex color string to RGB array with values 0-1
|
|
58
72
|
* @param hex - Hex color string like "#ffffff" or "ffffff"
|
|
@@ -70,8 +84,14 @@ interface RenderOptions {
|
|
|
70
84
|
lightDir: readonly [number, number, number];
|
|
71
85
|
camPos?: readonly [number, number, number] | null;
|
|
72
86
|
lookAt?: readonly [number, number, number] | null;
|
|
87
|
+
up: CameraUp;
|
|
88
|
+
cameraRotation: CameraRotation | null;
|
|
73
89
|
backgroundColor?: readonly [number, number, number] | string | null;
|
|
74
90
|
grid?: boolean | GridOptions;
|
|
91
|
+
debugPoints?: DebugPoint[] | null;
|
|
92
|
+
debugFontSize?: number | null;
|
|
93
|
+
debugPointColor?: readonly [number, number, number] | null;
|
|
94
|
+
debugLabelColor?: readonly [number, number, number] | null;
|
|
75
95
|
}
|
|
76
96
|
type RenderOptionsInput = Partial<RenderOptions>;
|
|
77
97
|
declare const DEFAULT_RENDER_OPTIONS: RenderOptions;
|
|
@@ -81,7 +101,7 @@ interface Camera {
|
|
|
81
101
|
view: mat4;
|
|
82
102
|
proj: mat4;
|
|
83
103
|
}
|
|
84
|
-
declare function buildCamera(drawCalls: DrawCall[], width: number, height: number, fovDeg: number, camPos: readonly [number, number, number] | null | undefined, lookAt: readonly [number, number, number] | null | undefined): Camera;
|
|
104
|
+
declare function buildCamera(drawCalls: DrawCall[], width: number, height: number, fovDeg: number, camPos: readonly [number, number, number] | null | undefined, lookAt: readonly [number, number, number] | null | undefined, up?: CameraUp | null | undefined, cameraRotation?: CameraRotation | null | undefined): Camera;
|
|
85
105
|
|
|
86
106
|
interface RenderResult {
|
|
87
107
|
bitmap: BitmapLike;
|
|
@@ -188,4 +208,4 @@ declare function renderGLTFToPNGBuffer(gltfPathOrString: string, options?: Rende
|
|
|
188
208
|
|
|
189
209
|
declare function renderSceneFromGLTF(scene: GLTFScene, options?: RenderOptionsInput, imageFactory?: ImageFactory): RenderResult;
|
|
190
210
|
|
|
191
|
-
export { type BitmapLike, type Camera, DEFAULT_LIGHT_DIR, DEFAULT_RENDER_OPTIONS, type DrawCall, type FetchLike, type GLTFResources, type GLTFScene, type GridOptions, type ImageFactory, type LightSettings, type LoadGLTFWithResourcesFromURLOptions, type Material, type MutableRGBA, type RGBA, type RenderGLTFToPNGBufferFromGLBBufferOptions, type RenderGLTFToPNGBufferFromURLOptions, type RenderOptions, type RenderOptionsInput, type RenderResult, SoftwareRenderer, bufferFromDataURI, buildCamera, computeSmoothNormals, computeWorldAABB, createGrid, createInfiniteGrid, createSceneFromGLTF, createUint8Bitmap, decodeImageFromBuffer, drawInfiniteGrid, encodePNGToBuffer, getDefaultRenderOptions, hexToRgb, isJPG, isPNG, loadGLTFWithResourcesFromURL, pureImageFactory, renderDrawCalls, renderGLTFToPNGBuffer, renderGLTFToPNGBufferFromGLBBuffer, renderGLTFToPNGBufferFromURL, renderSceneFromGLTF, resolveRenderOptions };
|
|
211
|
+
export { type BitmapLike, type Camera, type CameraRotation, type CameraUp, DEFAULT_LIGHT_DIR, DEFAULT_RENDER_OPTIONS, type DebugPoint, type DrawCall, type FetchLike, type GLTFResources, type GLTFScene, type GridOptions, type ImageFactory, type LightSettings, type LoadGLTFWithResourcesFromURLOptions, type Material, type MutableRGBA, type RGBA, type RenderGLTFToPNGBufferFromGLBBufferOptions, type RenderGLTFToPNGBufferFromURLOptions, type RenderOptions, type RenderOptionsInput, type RenderResult, SoftwareRenderer, bufferFromDataURI, buildCamera, computeSmoothNormals, computeWorldAABB, createGrid, createInfiniteGrid, createSceneFromGLTF, createUint8Bitmap, decodeImageFromBuffer, drawInfiniteGrid, encodePNGToBuffer, getDefaultRenderOptions, hexToRgb, isJPG, isPNG, loadGLTFWithResourcesFromURL, pureImageFactory, renderDrawCalls, renderGLTFToPNGBuffer, renderGLTFToPNGBufferFromGLBBuffer, renderGLTFToPNGBufferFromURL, renderSceneFromGLTF, resolveRenderOptions };
|
package/dist/index.js
CHANGED
|
@@ -36,49 +36,173 @@ function computeWorldAABB(drawCalls) {
|
|
|
36
36
|
var toRad = (degrees) => degrees * Math.PI / 180;
|
|
37
37
|
|
|
38
38
|
// lib/camera/buildCamera.ts
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
39
|
+
function getWorldUpVector(up) {
|
|
40
|
+
switch (up) {
|
|
41
|
+
case "y-":
|
|
42
|
+
return vec3.fromValues(0, -1, 0);
|
|
43
|
+
case "x+":
|
|
44
|
+
return vec3.fromValues(1, 0, 0);
|
|
45
|
+
case "x-":
|
|
46
|
+
return vec3.fromValues(-1, 0, 0);
|
|
47
|
+
case "z+":
|
|
48
|
+
return vec3.fromValues(0, 0, 1);
|
|
49
|
+
case "z-":
|
|
50
|
+
return vec3.fromValues(0, 0, -1);
|
|
51
|
+
case "y+":
|
|
52
|
+
default:
|
|
53
|
+
return vec3.fromValues(0, 1, 0);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function buildCameraWorldMatrix(eye, forward, up) {
|
|
57
|
+
const normalizedForward = vec3.normalize(vec3.create(), forward);
|
|
58
|
+
const right = vec3.cross(vec3.create(), normalizedForward, up);
|
|
59
|
+
if (vec3.squaredLength(right) === 0) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
vec3.normalize(right, right);
|
|
63
|
+
const correctedUp = vec3.cross(vec3.create(), right, normalizedForward);
|
|
64
|
+
if (vec3.squaredLength(correctedUp) === 0) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
vec3.normalize(correctedUp, correctedUp);
|
|
68
|
+
return mat4.fromValues(
|
|
69
|
+
right[0],
|
|
70
|
+
right[1],
|
|
71
|
+
right[2],
|
|
72
|
+
0,
|
|
73
|
+
correctedUp[0],
|
|
74
|
+
correctedUp[1],
|
|
75
|
+
correctedUp[2],
|
|
76
|
+
0,
|
|
77
|
+
-normalizedForward[0],
|
|
78
|
+
-normalizedForward[1],
|
|
79
|
+
-normalizedForward[2],
|
|
80
|
+
0,
|
|
81
|
+
eye[0],
|
|
82
|
+
eye[1],
|
|
83
|
+
eye[2],
|
|
84
|
+
1
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
function getDefaultForwardForUp(up) {
|
|
88
|
+
const zForward = vec3.fromValues(0, 0, -1);
|
|
89
|
+
if (Math.abs(vec3.dot(up, zForward)) < 0.999) {
|
|
90
|
+
return zForward;
|
|
91
|
+
}
|
|
92
|
+
return vec3.fromValues(1, 0, 0);
|
|
93
|
+
}
|
|
94
|
+
function buildViewFromRotation(eye, up, rotation) {
|
|
95
|
+
const cameraWorld = buildCameraWorldMatrix(
|
|
96
|
+
eye,
|
|
97
|
+
getDefaultForwardForUp(up),
|
|
98
|
+
up
|
|
99
|
+
);
|
|
100
|
+
if (!cameraWorld) {
|
|
101
|
+
return mat4.create();
|
|
102
|
+
}
|
|
103
|
+
if (rotation.x !== 0) {
|
|
104
|
+
mat4.rotateX(cameraWorld, cameraWorld, toRad(rotation.x));
|
|
105
|
+
}
|
|
106
|
+
if (rotation.y !== 0) {
|
|
107
|
+
mat4.rotateY(cameraWorld, cameraWorld, toRad(rotation.y));
|
|
108
|
+
}
|
|
109
|
+
if (rotation.z !== 0) {
|
|
110
|
+
mat4.rotateZ(cameraWorld, cameraWorld, toRad(rotation.z));
|
|
111
|
+
}
|
|
112
|
+
const view = mat4.create();
|
|
113
|
+
mat4.invert(view, cameraWorld);
|
|
114
|
+
return view;
|
|
115
|
+
}
|
|
116
|
+
function buildViewFromLookAt(eye, center, up) {
|
|
117
|
+
const forward = vec3.subtract(vec3.create(), center, eye);
|
|
118
|
+
if (vec3.squaredLength(forward) === 0) {
|
|
119
|
+
return mat4.create();
|
|
120
|
+
}
|
|
121
|
+
const cameraWorld = buildCameraWorldMatrix(eye, forward, up);
|
|
122
|
+
if (!cameraWorld) {
|
|
123
|
+
return mat4.create();
|
|
124
|
+
}
|
|
125
|
+
const view = mat4.create();
|
|
126
|
+
mat4.invert(view, cameraWorld);
|
|
127
|
+
return view;
|
|
128
|
+
}
|
|
129
|
+
function getAutoDistance(drawCalls, fovDeg) {
|
|
130
|
+
const aabb = computeWorldAABB(drawCalls);
|
|
131
|
+
const diag = vec3.distance(
|
|
132
|
+
vec3.fromValues(aabb.min[0], aabb.min[1], aabb.min[2]),
|
|
133
|
+
vec3.fromValues(aabb.max[0], aabb.max[1], aabb.max[2])
|
|
134
|
+
);
|
|
135
|
+
const radius = diag * 0.5;
|
|
136
|
+
const fov = toRad(fovDeg);
|
|
137
|
+
return radius / Math.tan(fov * 0.5) + radius * 0.5;
|
|
138
|
+
}
|
|
139
|
+
function getAABBCornerDistances(aabb, eye) {
|
|
140
|
+
const xs = [aabb.min[0], aabb.max[0]];
|
|
141
|
+
const ys = [aabb.min[1], aabb.max[1]];
|
|
142
|
+
const zs = [aabb.min[2], aabb.max[2]];
|
|
143
|
+
let nearest = Infinity;
|
|
144
|
+
let farthest = 0;
|
|
145
|
+
for (const x of xs) {
|
|
146
|
+
for (const y of ys) {
|
|
147
|
+
for (const z of zs) {
|
|
148
|
+
const distance = vec3.distance(eye, vec3.fromValues(x, y, z));
|
|
149
|
+
nearest = Math.min(nearest, distance);
|
|
150
|
+
farthest = Math.max(farthest, distance);
|
|
151
|
+
}
|
|
58
152
|
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
153
|
+
}
|
|
154
|
+
return { nearest, farthest };
|
|
155
|
+
}
|
|
156
|
+
function getCameraForwardFromView(view) {
|
|
157
|
+
const cameraWorld = mat4.create();
|
|
158
|
+
mat4.invert(cameraWorld, view);
|
|
159
|
+
return vec3.fromValues(-cameraWorld[8], -cameraWorld[9], -cameraWorld[10]);
|
|
160
|
+
}
|
|
161
|
+
function buildCamera(drawCalls, width, height, fovDeg, camPos, lookAt, up = "y+", cameraRotation = null) {
|
|
162
|
+
const aspect = width / height;
|
|
163
|
+
const worldUp = getWorldUpVector(up);
|
|
164
|
+
const aabb = computeWorldAABB(drawCalls);
|
|
165
|
+
const center = vec3.fromValues(
|
|
166
|
+
0.5 * (aabb.min[0] + aabb.max[0]),
|
|
167
|
+
0.5 * (aabb.min[1] + aabb.max[1]),
|
|
168
|
+
0.5 * (aabb.min[2] + aabb.max[2])
|
|
169
|
+
);
|
|
170
|
+
const autoDistance = getAutoDistance(drawCalls, fovDeg);
|
|
171
|
+
if (cameraRotation) {
|
|
172
|
+
const provisionalEye = camPos ? vec3.fromValues(camPos[0], camPos[1], camPos[2]) : vec3.clone(center);
|
|
173
|
+
const provisionalView = buildViewFromRotation(
|
|
174
|
+
provisionalEye,
|
|
175
|
+
worldUp,
|
|
176
|
+
cameraRotation
|
|
69
177
|
);
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
center[1] + dist * 0.3,
|
|
76
|
-
center[2] + dist
|
|
178
|
+
const eye2 = camPos ? provisionalEye : vec3.scaleAndAdd(
|
|
179
|
+
vec3.create(),
|
|
180
|
+
center,
|
|
181
|
+
getCameraForwardFromView(provisionalView),
|
|
182
|
+
-autoDistance
|
|
77
183
|
);
|
|
184
|
+
const { nearest: nearest2, farthest: farthest2 } = getAABBCornerDistances(aabb, eye2);
|
|
185
|
+
const near2 = Math.max(0.01, nearest2 * 0.5);
|
|
186
|
+
const far2 = Math.max(1e3, farthest2 * 1.1);
|
|
187
|
+
const proj2 = mat4.create();
|
|
188
|
+
mat4.perspective(proj2, toRad(fovDeg), aspect, near2, far2);
|
|
189
|
+
return {
|
|
190
|
+
view: buildViewFromRotation(eye2, worldUp, cameraRotation),
|
|
191
|
+
proj: proj2
|
|
192
|
+
};
|
|
78
193
|
}
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
194
|
+
const eye = camPos ? vec3.fromValues(camPos[0], camPos[1], camPos[2]) : vec3.fromValues(
|
|
195
|
+
center[0] + autoDistance,
|
|
196
|
+
center[1] + autoDistance * 0.3,
|
|
197
|
+
center[2] + autoDistance
|
|
198
|
+
);
|
|
199
|
+
const target = lookAt ? vec3.fromValues(lookAt[0], lookAt[1], lookAt[2]) : center;
|
|
200
|
+
const { nearest, farthest } = getAABBCornerDistances(aabb, eye);
|
|
201
|
+
const near = Math.max(0.01, nearest * 0.5);
|
|
202
|
+
const far = Math.max(1e3, farthest * 1.1);
|
|
203
|
+
const proj = mat4.create();
|
|
204
|
+
mat4.perspective(proj, toRad(fovDeg), aspect, near, far);
|
|
205
|
+
const view = buildViewFromLookAt(eye, target, worldUp);
|
|
82
206
|
return { view, proj };
|
|
83
207
|
}
|
|
84
208
|
|
|
@@ -174,207 +298,16 @@ function downsampleBitmap(source, targetWidth, targetHeight, imageFactory) {
|
|
|
174
298
|
return target;
|
|
175
299
|
}
|
|
176
300
|
|
|
177
|
-
// lib/render/
|
|
178
|
-
import {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// lib/utils/clamp.ts
|
|
187
|
-
var clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
188
|
-
|
|
189
|
-
// lib/render/drawInfiniteGrid.ts
|
|
190
|
-
function computeGridLineIntensity(params) {
|
|
191
|
-
const rx = params.world_x / params.cell_size;
|
|
192
|
-
const rz = params.world_z / params.cell_size;
|
|
193
|
-
const fract_x = rx - Math.floor(rx + 0.5);
|
|
194
|
-
const fract_z = rz - Math.floor(rz + 0.5);
|
|
195
|
-
const grid_x = Math.abs(fract_x);
|
|
196
|
-
const grid_z = Math.abs(fract_z);
|
|
197
|
-
const pixel_size_in_world = params.dist / Math.min(params.screen_width, params.screen_height);
|
|
198
|
-
const fwidth_x = pixel_size_in_world / params.cell_size;
|
|
199
|
-
const fwidth_z = pixel_size_in_world / params.cell_size;
|
|
200
|
-
const line_x = grid_x / (fwidth_x + 1e-4);
|
|
201
|
-
const line_z = grid_z / (fwidth_z + 1e-4);
|
|
202
|
-
const line = Math.min(line_x, line_z);
|
|
203
|
-
return 1 - Math.min(line * params.line_thickness_multiplier, 1);
|
|
204
|
-
}
|
|
205
|
-
function computeGridFadeAlpha(params) {
|
|
206
|
-
const fade_start = params.fade_distance;
|
|
207
|
-
const fade_end = params.fade_distance * params.fade_strength;
|
|
208
|
-
return 1 - Math.min(
|
|
209
|
-
1,
|
|
210
|
-
Math.max(0, (params.dist - fade_start) / (fade_end - fade_start))
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
function drawInfiniteGrid(software_renderer, params) {
|
|
214
|
-
const grid_y = params.grid_y ?? 0;
|
|
215
|
-
const cell_size = params.cell_size ?? 1;
|
|
216
|
-
const section_size = params.section_size ?? 10;
|
|
217
|
-
const fade_distance = params.fade_distance ?? 100;
|
|
218
|
-
const fade_strength = params.fade_strength ?? 1.5;
|
|
219
|
-
const grid_color = params.grid_color ?? [0.93, 0.93, 0.93];
|
|
220
|
-
const section_color = params.section_color ?? [0.8, 0.8, 1];
|
|
221
|
-
const gamma_out = params.gamma_out ?? true;
|
|
222
|
-
const camera = params.camera;
|
|
223
|
-
const view = camera.view;
|
|
224
|
-
const proj = camera.proj;
|
|
225
|
-
const inv_view = mat43.create();
|
|
226
|
-
mat43.invert(inv_view, view);
|
|
227
|
-
const cam_pos_x = inv_view[12];
|
|
228
|
-
const cam_pos_y = inv_view[13];
|
|
229
|
-
const cam_pos_z = inv_view[14];
|
|
230
|
-
const vp = mat43.create();
|
|
231
|
-
mat43.multiply(vp, proj, view);
|
|
232
|
-
const inv_vp = mat43.create();
|
|
233
|
-
mat43.invert(inv_vp, vp);
|
|
234
|
-
for (let y = 0; y < software_renderer.height; y++) {
|
|
235
|
-
for (let x = 0; x < software_renderer.width; x++) {
|
|
236
|
-
const ndc_x = x / (software_renderer.width - 1) * 2 - 1;
|
|
237
|
-
const ndc_y = 1 - y / (software_renderer.height - 1) * 2;
|
|
238
|
-
const near_point = vec42.fromValues(ndc_x, ndc_y, -1, 1);
|
|
239
|
-
const far_point = vec42.fromValues(ndc_x, ndc_y, 1, 1);
|
|
240
|
-
vec42.transformMat4(near_point, near_point, inv_vp);
|
|
241
|
-
vec42.transformMat4(far_point, far_point, inv_vp);
|
|
242
|
-
near_point[0] /= near_point[3];
|
|
243
|
-
near_point[1] /= near_point[3];
|
|
244
|
-
near_point[2] /= near_point[3];
|
|
245
|
-
far_point[0] /= far_point[3];
|
|
246
|
-
far_point[1] /= far_point[3];
|
|
247
|
-
far_point[2] /= far_point[3];
|
|
248
|
-
const ray_dir_x = far_point[0] - near_point[0];
|
|
249
|
-
const ray_dir_y = far_point[1] - near_point[1];
|
|
250
|
-
const ray_dir_z = far_point[2] - near_point[2];
|
|
251
|
-
if (Math.abs(ray_dir_y) < 1e-6) continue;
|
|
252
|
-
const t = (grid_y - near_point[1]) / ray_dir_y;
|
|
253
|
-
if (t < 0 || t > 1) continue;
|
|
254
|
-
const world_x = near_point[0] + t * ray_dir_x;
|
|
255
|
-
const world_z = near_point[2] + t * ray_dir_z;
|
|
256
|
-
const world_pt = vec42.fromValues(world_x, grid_y, world_z, 1);
|
|
257
|
-
const clip_pt = vec42.create();
|
|
258
|
-
vec42.transformMat4(clip_pt, world_pt, vp);
|
|
259
|
-
const ndc_z = clip_pt[2] / clip_pt[3];
|
|
260
|
-
const depth_01 = ndc_z * 0.5 + 0.5;
|
|
261
|
-
const di = y * software_renderer.width + x;
|
|
262
|
-
if (depth_01 >= software_renderer.depth[di]) continue;
|
|
263
|
-
const dx = world_x - cam_pos_x;
|
|
264
|
-
const dy = grid_y - cam_pos_y;
|
|
265
|
-
const dz = world_z - cam_pos_z;
|
|
266
|
-
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
267
|
-
const alpha = computeGridFadeAlpha({
|
|
268
|
-
dist,
|
|
269
|
-
fade_distance,
|
|
270
|
-
fade_strength
|
|
271
|
-
});
|
|
272
|
-
if (alpha <= 0) continue;
|
|
273
|
-
const g1 = computeGridLineIntensity({
|
|
274
|
-
world_x,
|
|
275
|
-
world_z,
|
|
276
|
-
cell_size,
|
|
277
|
-
dist,
|
|
278
|
-
screen_width: software_renderer.width,
|
|
279
|
-
screen_height: software_renderer.height,
|
|
280
|
-
line_thickness_multiplier: 0.8
|
|
281
|
-
});
|
|
282
|
-
const g2 = computeGridLineIntensity({
|
|
283
|
-
world_x,
|
|
284
|
-
world_z,
|
|
285
|
-
cell_size: section_size,
|
|
286
|
-
dist,
|
|
287
|
-
screen_width: software_renderer.width,
|
|
288
|
-
screen_height: software_renderer.height,
|
|
289
|
-
line_thickness_multiplier: 0.5
|
|
290
|
-
// Thicker section lines
|
|
291
|
-
});
|
|
292
|
-
const grid_strength = Math.max(g1, g2);
|
|
293
|
-
if (grid_strength <= 0) continue;
|
|
294
|
-
const final_r = grid_color[0] * (1 - g2) + section_color[0] * g2;
|
|
295
|
-
const final_g = grid_color[1] * (1 - g2) + section_color[1] * g2;
|
|
296
|
-
const final_b = grid_color[2] * (1 - g2) + section_color[2] * g2;
|
|
297
|
-
const final_alpha = grid_strength * alpha;
|
|
298
|
-
if (final_alpha <= 0.01) continue;
|
|
299
|
-
let out_r = final_r;
|
|
300
|
-
let out_g = final_g;
|
|
301
|
-
let out_b = final_b;
|
|
302
|
-
if (gamma_out) {
|
|
303
|
-
out_r = srgbEncodeLinear01(clamp(out_r, 0, 1));
|
|
304
|
-
out_g = srgbEncodeLinear01(clamp(out_g, 0, 1));
|
|
305
|
-
out_b = srgbEncodeLinear01(clamp(out_b, 0, 1));
|
|
306
|
-
} else {
|
|
307
|
-
out_r = clamp(out_r, 0, 1);
|
|
308
|
-
out_g = clamp(out_g, 0, 1);
|
|
309
|
-
out_b = clamp(out_b, 0, 1);
|
|
310
|
-
}
|
|
311
|
-
const dst_idx = (y * software_renderer.width + x) * 4;
|
|
312
|
-
const dst_r = (software_renderer.buffer[dst_idx + 0] ?? 0) / 255;
|
|
313
|
-
const dst_g = (software_renderer.buffer[dst_idx + 1] ?? 0) / 255;
|
|
314
|
-
const dst_b = (software_renderer.buffer[dst_idx + 2] ?? 0) / 255;
|
|
315
|
-
const dst_a = (software_renderer.buffer[dst_idx + 3] ?? 0) / 255;
|
|
316
|
-
const one_minus_a = 1 - final_alpha;
|
|
317
|
-
const blend_r = out_r * final_alpha + dst_r * one_minus_a;
|
|
318
|
-
const blend_g = out_g * final_alpha + dst_g * one_minus_a;
|
|
319
|
-
const blend_b = out_b * final_alpha + dst_b * one_minus_a;
|
|
320
|
-
const blend_a = final_alpha + dst_a * one_minus_a;
|
|
321
|
-
software_renderer.setPixel(
|
|
322
|
-
x,
|
|
323
|
-
y,
|
|
324
|
-
clamp(blend_r, 0, 1) * 255 | 0,
|
|
325
|
-
clamp(blend_g, 0, 1) * 255 | 0,
|
|
326
|
-
clamp(blend_b, 0, 1) * 255 | 0,
|
|
327
|
-
clamp(blend_a, 0, 1) * 255 | 0
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// lib/render/getDefaultRenderOptions.ts
|
|
334
|
-
var DEFAULT_LIGHT_DIR = [-0.4, -0.9, -0.2];
|
|
335
|
-
function hexToRgb(hex) {
|
|
336
|
-
const cleanHex = hex.replace(/^#/, "");
|
|
337
|
-
if (!/^[0-9A-Fa-f]{6}$/.test(cleanHex)) {
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
340
|
-
const r = Number.parseInt(cleanHex.substring(0, 2), 16) / 255;
|
|
341
|
-
const g = Number.parseInt(cleanHex.substring(2, 4), 16) / 255;
|
|
342
|
-
const b = Number.parseInt(cleanHex.substring(4, 6), 16) / 255;
|
|
343
|
-
return [r, g, b];
|
|
344
|
-
}
|
|
345
|
-
var DEFAULT_RENDER_OPTIONS = {
|
|
346
|
-
width: 800,
|
|
347
|
-
height: 600,
|
|
348
|
-
supersampling: 1,
|
|
349
|
-
fov: 60,
|
|
350
|
-
cull: true,
|
|
351
|
-
gamma: true,
|
|
352
|
-
ambient: 0.15,
|
|
353
|
-
lightDir: DEFAULT_LIGHT_DIR,
|
|
354
|
-
camPos: null,
|
|
355
|
-
lookAt: null,
|
|
356
|
-
backgroundColor: null,
|
|
357
|
-
grid: false
|
|
358
|
-
};
|
|
359
|
-
function getDefaultRenderOptions() {
|
|
360
|
-
return { ...DEFAULT_RENDER_OPTIONS };
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// lib/render/resolveRenderOptions.ts
|
|
364
|
-
function resolveRenderOptions(options = {}) {
|
|
365
|
-
const supersampling = typeof options.supersampling === "number" && Number.isFinite(options.supersampling) ? Math.max(1, Math.floor(options.supersampling)) : DEFAULT_RENDER_OPTIONS.supersampling;
|
|
366
|
-
return {
|
|
367
|
-
...DEFAULT_RENDER_OPTIONS,
|
|
368
|
-
...options,
|
|
369
|
-
supersampling,
|
|
370
|
-
lightDir: options.lightDir != null ? options.lightDir : DEFAULT_RENDER_OPTIONS.lightDir,
|
|
371
|
-
camPos: options.camPos !== void 0 ? options.camPos : DEFAULT_RENDER_OPTIONS.camPos,
|
|
372
|
-
lookAt: options.lookAt !== void 0 ? options.lookAt : DEFAULT_RENDER_OPTIONS.lookAt
|
|
373
|
-
};
|
|
374
|
-
}
|
|
301
|
+
// lib/render/drawDebugPoints.ts
|
|
302
|
+
import {
|
|
303
|
+
glyphAdvanceRatio,
|
|
304
|
+
glyphLineAlphabet,
|
|
305
|
+
textMetrics
|
|
306
|
+
} from "@tscircuit/alphabet";
|
|
307
|
+
import { mat4 as mat44, vec4 as vec43 } from "gl-matrix";
|
|
375
308
|
|
|
376
309
|
// lib/render/SoftwareRenderer.ts
|
|
377
|
-
import { mat3, mat4 as
|
|
310
|
+
import { mat3, mat4 as mat43, vec3 as vec32, vec4 as vec42 } from "gl-matrix";
|
|
378
311
|
|
|
379
312
|
// lib/gltf/computeSmoothNormals.ts
|
|
380
313
|
function computeSmoothNormals(positions, indices) {
|
|
@@ -442,11 +375,56 @@ function computeSmoothNormals(positions, indices) {
|
|
|
442
375
|
return normals;
|
|
443
376
|
}
|
|
444
377
|
|
|
378
|
+
// lib/render/getDefaultRenderOptions.ts
|
|
379
|
+
var DEFAULT_LIGHT_DIR = [-0.4, -0.9, -0.2];
|
|
380
|
+
function hexToRgb(hex) {
|
|
381
|
+
const cleanHex = hex.replace(/^#/, "");
|
|
382
|
+
if (!/^[0-9A-Fa-f]{6}$/.test(cleanHex)) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
const r = Number.parseInt(cleanHex.substring(0, 2), 16) / 255;
|
|
386
|
+
const g = Number.parseInt(cleanHex.substring(2, 4), 16) / 255;
|
|
387
|
+
const b = Number.parseInt(cleanHex.substring(4, 6), 16) / 255;
|
|
388
|
+
return [r, g, b];
|
|
389
|
+
}
|
|
390
|
+
var DEFAULT_RENDER_OPTIONS = {
|
|
391
|
+
width: 800,
|
|
392
|
+
height: 600,
|
|
393
|
+
supersampling: 1,
|
|
394
|
+
fov: 60,
|
|
395
|
+
cull: true,
|
|
396
|
+
gamma: true,
|
|
397
|
+
ambient: 0.15,
|
|
398
|
+
lightDir: DEFAULT_LIGHT_DIR,
|
|
399
|
+
camPos: null,
|
|
400
|
+
lookAt: null,
|
|
401
|
+
up: "y+",
|
|
402
|
+
cameraRotation: null,
|
|
403
|
+
backgroundColor: null,
|
|
404
|
+
grid: false,
|
|
405
|
+
debugPoints: null,
|
|
406
|
+
debugFontSize: null,
|
|
407
|
+
debugPointColor: null,
|
|
408
|
+
debugLabelColor: null
|
|
409
|
+
};
|
|
410
|
+
function getDefaultRenderOptions() {
|
|
411
|
+
return { ...DEFAULT_RENDER_OPTIONS };
|
|
412
|
+
}
|
|
413
|
+
|
|
445
414
|
// lib/utils/mulColor.ts
|
|
446
415
|
function mulColor(a, b) {
|
|
447
416
|
return [a[0] * b[0], a[1] * b[1], a[2] * b[2], a[3] * b[3]];
|
|
448
417
|
}
|
|
449
418
|
|
|
419
|
+
// lib/utils/srgbEncodeLinear01.ts
|
|
420
|
+
function srgbEncodeLinear01(linear) {
|
|
421
|
+
if (linear <= 31308e-7) return 12.92 * linear;
|
|
422
|
+
return 1.055 * Math.pow(linear, 1 / 2.4) - 0.055;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// lib/utils/clamp.ts
|
|
426
|
+
var clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
427
|
+
|
|
450
428
|
// lib/render/SoftwareRenderer.ts
|
|
451
429
|
var SoftwareRenderer = class {
|
|
452
430
|
width;
|
|
@@ -486,21 +464,21 @@ var SoftwareRenderer = class {
|
|
|
486
464
|
if (!indices) return;
|
|
487
465
|
const view = camera.view;
|
|
488
466
|
const proj = camera.proj;
|
|
489
|
-
const mvp =
|
|
490
|
-
|
|
467
|
+
const mvp = mat43.create();
|
|
468
|
+
mat43.multiply(mvp, proj, mat43.multiply(mat43.create(), view, model));
|
|
491
469
|
const vertexCount = positions.length / 3 | 0;
|
|
492
470
|
const vScreen = new Array(vertexCount);
|
|
493
471
|
const vInvW = new Float32Array(vertexCount);
|
|
494
472
|
const vNDCz = new Float32Array(vertexCount);
|
|
495
473
|
for (let i = 0; i < vertexCount; i++) {
|
|
496
|
-
const p =
|
|
474
|
+
const p = vec42.fromValues(
|
|
497
475
|
positions[i * 3 + 0],
|
|
498
476
|
positions[i * 3 + 1],
|
|
499
477
|
positions[i * 3 + 2],
|
|
500
478
|
1
|
|
501
479
|
);
|
|
502
|
-
const c =
|
|
503
|
-
|
|
480
|
+
const c = vec42.create();
|
|
481
|
+
vec42.transformMat4(c, p, mvp);
|
|
504
482
|
const invW = 1 / c[3];
|
|
505
483
|
vInvW[i] = invW;
|
|
506
484
|
if (!isFinite(invW)) {
|
|
@@ -600,8 +578,8 @@ var SoftwareRenderer = class {
|
|
|
600
578
|
const { positions, normals, uvs, indices, model, colors } = mesh;
|
|
601
579
|
const view = camera.view;
|
|
602
580
|
const proj = camera.proj;
|
|
603
|
-
const mvp =
|
|
604
|
-
|
|
581
|
+
const mvp = mat43.create();
|
|
582
|
+
mat43.multiply(mvp, proj, mat43.multiply(mat43.create(), view, model));
|
|
605
583
|
const normalMat = mat3.create();
|
|
606
584
|
mat3.normalFromMat4(normalMat, model);
|
|
607
585
|
const vertexCount = positions.length / 3 | 0;
|
|
@@ -620,20 +598,20 @@ var SoftwareRenderer = class {
|
|
|
620
598
|
const vWorldN = new Array(vertexCount);
|
|
621
599
|
const vColor = new Array(vertexCount);
|
|
622
600
|
for (let i = 0; i < vertexCount; i++) {
|
|
623
|
-
const p =
|
|
601
|
+
const p = vec42.fromValues(
|
|
624
602
|
positions[i * 3 + 0],
|
|
625
603
|
positions[i * 3 + 1],
|
|
626
604
|
positions[i * 3 + 2],
|
|
627
605
|
1
|
|
628
606
|
);
|
|
629
|
-
const c =
|
|
630
|
-
|
|
607
|
+
const c = vec42.create();
|
|
608
|
+
vec42.transformMat4(c, p, mvp);
|
|
631
609
|
const invW = 1 / c[3];
|
|
632
610
|
const ndcX = c[0] * invW;
|
|
633
611
|
const ndcY = c[1] * invW;
|
|
634
612
|
const ndcZ = c[2] * invW;
|
|
635
|
-
const sx =
|
|
636
|
-
const sy =
|
|
613
|
+
const sx = (ndcX * 0.5 + 0.5) * (this.width - 1);
|
|
614
|
+
const sy = (1 - (ndcY * 0.5 + 0.5)) * (this.height - 1);
|
|
637
615
|
vScreen[i] = [sx, sy];
|
|
638
616
|
vInvW[i] = invW;
|
|
639
617
|
vNDCz[i] = ndcZ;
|
|
@@ -663,10 +641,16 @@ var SoftwareRenderer = class {
|
|
|
663
641
|
const area = edge(v0, v1, v2);
|
|
664
642
|
if (area === 0) continue;
|
|
665
643
|
if (cullBackFaces && area < 0) continue;
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
644
|
+
const minX = Math.max(0, Math.floor(Math.min(v0[0], v1[0], v2[0])));
|
|
645
|
+
const maxX = Math.min(
|
|
646
|
+
this.width - 1,
|
|
647
|
+
Math.ceil(Math.max(v0[0], v1[0], v2[0]))
|
|
648
|
+
);
|
|
649
|
+
const minY = Math.max(0, Math.floor(Math.min(v0[1], v1[1], v2[1])));
|
|
650
|
+
const maxY = Math.min(
|
|
651
|
+
this.height - 1,
|
|
652
|
+
Math.ceil(Math.max(v0[1], v1[1], v2[1]))
|
|
653
|
+
);
|
|
670
654
|
const invW = [
|
|
671
655
|
vInvW[i0],
|
|
672
656
|
vInvW[i1],
|
|
@@ -705,6 +689,7 @@ var SoftwareRenderer = class {
|
|
|
705
689
|
const l2 = w2 * invArea;
|
|
706
690
|
const zndc = l0 * ndcZ[0] + l1 * ndcZ[1] + l2 * ndcZ[2];
|
|
707
691
|
const z01 = zndc * 0.5 + 0.5;
|
|
692
|
+
if (z01 < 0 || z01 > 1) continue;
|
|
708
693
|
const di = y * this.width + x;
|
|
709
694
|
const depth = this.depth;
|
|
710
695
|
if (z01 >= depth[di]) continue;
|
|
@@ -806,6 +791,350 @@ function edge(a, b, p) {
|
|
|
806
791
|
return (p[0] - a[0]) * (b[1] - a[1]) - (p[1] - a[1]) * (b[0] - a[0]);
|
|
807
792
|
}
|
|
808
793
|
|
|
794
|
+
// lib/render/drawDebugPoints.ts
|
|
795
|
+
var DEFAULT_MARKER_COLOR = [
|
|
796
|
+
255,
|
|
797
|
+
0,
|
|
798
|
+
170,
|
|
799
|
+
255
|
|
800
|
+
];
|
|
801
|
+
var DEFAULT_TEXT_COLOR = [
|
|
802
|
+
255,
|
|
803
|
+
24,
|
|
804
|
+
170,
|
|
805
|
+
255
|
|
806
|
+
];
|
|
807
|
+
var CHARACTER_WIDTH_RATIO = 0.62;
|
|
808
|
+
function drawDebugPoints(renderer, camera, debugPoints, debugFontSize, debugPointColor, debugLabelColor) {
|
|
809
|
+
if (debugPoints.length === 0) return;
|
|
810
|
+
const viewProj = mat44.create();
|
|
811
|
+
mat44.multiply(viewProj, camera.proj, camera.view);
|
|
812
|
+
const markerColor = toColorRGBA(debugPointColor, DEFAULT_MARKER_COLOR);
|
|
813
|
+
const textColor = toColorRGBA(debugLabelColor, DEFAULT_TEXT_COLOR);
|
|
814
|
+
const labelScale = typeof debugFontSize === "number" && Number.isFinite(debugFontSize) ? Math.max(1, Math.round(debugFontSize)) : Math.max(
|
|
815
|
+
12,
|
|
816
|
+
Math.round(Math.min(renderer.width, renderer.height) * 0.028)
|
|
817
|
+
);
|
|
818
|
+
const textThickness = Math.max(
|
|
819
|
+
2,
|
|
820
|
+
Math.round(labelScale * textMetrics.strokeWidthRatio)
|
|
821
|
+
);
|
|
822
|
+
for (const debugPoint of debugPoints) {
|
|
823
|
+
const projected = projectWorldToScreen(
|
|
824
|
+
debugPoint.position,
|
|
825
|
+
viewProj,
|
|
826
|
+
renderer
|
|
827
|
+
);
|
|
828
|
+
if (!projected) continue;
|
|
829
|
+
drawMarker(renderer, projected, markerColor);
|
|
830
|
+
drawLabel(
|
|
831
|
+
renderer,
|
|
832
|
+
projected,
|
|
833
|
+
debugPoint.label,
|
|
834
|
+
textColor,
|
|
835
|
+
labelScale,
|
|
836
|
+
textThickness
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function projectWorldToScreen(position, viewProj, renderer) {
|
|
841
|
+
const clip = vec43.fromValues(position.x, position.y, position.z, 1);
|
|
842
|
+
vec43.transformMat4(clip, clip, viewProj);
|
|
843
|
+
if (!Number.isFinite(clip[3]) || clip[3] <= 0) return null;
|
|
844
|
+
const invW = 1 / clip[3];
|
|
845
|
+
const ndcX = clip[0] * invW;
|
|
846
|
+
const ndcY = clip[1] * invW;
|
|
847
|
+
const ndcZ = clip[2] * invW;
|
|
848
|
+
if (!Number.isFinite(ndcX) || !Number.isFinite(ndcY) || !Number.isFinite(ndcZ) || ndcZ < -1 || ndcZ > 1) {
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
return {
|
|
852
|
+
x: (ndcX * 0.5 + 0.5) * (renderer.width - 1),
|
|
853
|
+
y: (1 - (ndcY * 0.5 + 0.5)) * (renderer.height - 1)
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
function drawMarker(renderer, projected, color) {
|
|
857
|
+
const centerX = Math.round(projected.x);
|
|
858
|
+
const centerY = Math.round(projected.y);
|
|
859
|
+
for (let offset = -5; offset <= 4; offset += 1) {
|
|
860
|
+
renderer.setPixel(
|
|
861
|
+
centerX + offset,
|
|
862
|
+
centerY,
|
|
863
|
+
color[0],
|
|
864
|
+
color[1],
|
|
865
|
+
color[2],
|
|
866
|
+
color[3]
|
|
867
|
+
);
|
|
868
|
+
renderer.setPixel(
|
|
869
|
+
centerX,
|
|
870
|
+
centerY + offset,
|
|
871
|
+
color[0],
|
|
872
|
+
color[1],
|
|
873
|
+
color[2],
|
|
874
|
+
color[3]
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
function drawLabel(renderer, projected, label, color, scale, textThickness) {
|
|
879
|
+
if (label.length === 0) return;
|
|
880
|
+
const scaleX = scale * CHARACTER_WIDTH_RATIO;
|
|
881
|
+
const labelWidth = measureLabelWidth(label, scaleX);
|
|
882
|
+
let x = projected.x + scale * 0.6;
|
|
883
|
+
let y = projected.y - scale * 1.1;
|
|
884
|
+
if (x + labelWidth > renderer.width - 2) {
|
|
885
|
+
x = projected.x - scale * 0.6 - labelWidth;
|
|
886
|
+
}
|
|
887
|
+
if (y < 2) {
|
|
888
|
+
y = projected.y + scale * 0.2;
|
|
889
|
+
}
|
|
890
|
+
drawStrokeText(renderer, label, x, y, scaleX, scale, color, textThickness);
|
|
891
|
+
}
|
|
892
|
+
function drawStrokeText(renderer, text, x, y, scaleX, scaleY, color, thickness) {
|
|
893
|
+
let cursorX = x;
|
|
894
|
+
for (const rawChar of text) {
|
|
895
|
+
const char = resolveGlyph(rawChar);
|
|
896
|
+
const glyph = glyphLineAlphabet[char];
|
|
897
|
+
const advanceRatio = glyphAdvanceRatio[char] ?? (char === " " ? textMetrics.spaceWidthRatio : textMetrics.glyphWidthRatio);
|
|
898
|
+
if (glyph) {
|
|
899
|
+
for (const segment of glyph) {
|
|
900
|
+
drawLine(
|
|
901
|
+
renderer,
|
|
902
|
+
cursorX + segment.x1 * scaleX,
|
|
903
|
+
y + (1 - segment.y1) * scaleY,
|
|
904
|
+
cursorX + segment.x2 * scaleX,
|
|
905
|
+
y + (1 - segment.y2) * scaleY,
|
|
906
|
+
color,
|
|
907
|
+
thickness
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
cursorX += advanceRatio * scaleX;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
function resolveGlyph(char) {
|
|
915
|
+
if (glyphLineAlphabet[char]) return char;
|
|
916
|
+
const uppercase = char.toUpperCase();
|
|
917
|
+
if (glyphLineAlphabet[uppercase]) return uppercase;
|
|
918
|
+
return "?";
|
|
919
|
+
}
|
|
920
|
+
function measureLabelWidth(text, scaleX) {
|
|
921
|
+
let width = 0;
|
|
922
|
+
for (const rawChar of text) {
|
|
923
|
+
const char = resolveGlyph(rawChar);
|
|
924
|
+
width += glyphAdvanceRatio[char] ?? (char === " " ? textMetrics.spaceWidthRatio : textMetrics.glyphWidthRatio);
|
|
925
|
+
}
|
|
926
|
+
return width * scaleX;
|
|
927
|
+
}
|
|
928
|
+
function drawLine(renderer, x0, y0, x1, y1, color, thickness) {
|
|
929
|
+
const dx = x1 - x0;
|
|
930
|
+
const dy = y1 - y0;
|
|
931
|
+
const steps = Math.max(Math.abs(dx), Math.abs(dy));
|
|
932
|
+
if (steps === 0) {
|
|
933
|
+
drawBrush(renderer, x0, y0, color, thickness);
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
for (let step = 0; step <= steps; step += 1) {
|
|
937
|
+
const t = step / steps;
|
|
938
|
+
drawBrush(renderer, x0 + dx * t, y0 + dy * t, color, thickness);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
function drawBrush(renderer, x, y, color, thickness) {
|
|
942
|
+
const radius = Math.max(0, Math.floor((thickness - 1) / 2));
|
|
943
|
+
const centerX = Math.round(x);
|
|
944
|
+
const centerY = Math.round(y);
|
|
945
|
+
for (let offsetY = -radius; offsetY <= radius; offsetY += 1) {
|
|
946
|
+
for (let offsetX = -radius; offsetX <= radius; offsetX += 1) {
|
|
947
|
+
renderer.setPixel(
|
|
948
|
+
centerX + offsetX,
|
|
949
|
+
centerY + offsetY,
|
|
950
|
+
color[0],
|
|
951
|
+
color[1],
|
|
952
|
+
color[2],
|
|
953
|
+
color[3]
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
function toColorRGBA(color, fallback) {
|
|
959
|
+
if (!color) return fallback;
|
|
960
|
+
return [color[0], color[1], color[2], 255];
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// lib/render/drawInfiniteGrid.ts
|
|
964
|
+
import { mat4 as mat45, vec4 as vec44 } from "gl-matrix";
|
|
965
|
+
function computeGridLineIntensity(params) {
|
|
966
|
+
const rx = params.world_x / params.cell_size;
|
|
967
|
+
const rz = params.world_z / params.cell_size;
|
|
968
|
+
const fract_x = rx - Math.floor(rx + 0.5);
|
|
969
|
+
const fract_z = rz - Math.floor(rz + 0.5);
|
|
970
|
+
const grid_x = Math.abs(fract_x);
|
|
971
|
+
const grid_z = Math.abs(fract_z);
|
|
972
|
+
const pixel_size_in_world = params.dist / Math.min(params.screen_width, params.screen_height);
|
|
973
|
+
const fwidth_x = pixel_size_in_world / params.cell_size;
|
|
974
|
+
const fwidth_z = pixel_size_in_world / params.cell_size;
|
|
975
|
+
const line_x = grid_x / (fwidth_x + 1e-4);
|
|
976
|
+
const line_z = grid_z / (fwidth_z + 1e-4);
|
|
977
|
+
const line = Math.min(line_x, line_z);
|
|
978
|
+
return 1 - Math.min(line * params.line_thickness_multiplier, 1);
|
|
979
|
+
}
|
|
980
|
+
function computeGridFadeAlpha(params) {
|
|
981
|
+
const fade_start = params.fade_distance;
|
|
982
|
+
const fade_end = params.fade_distance * params.fade_strength;
|
|
983
|
+
return 1 - Math.min(
|
|
984
|
+
1,
|
|
985
|
+
Math.max(0, (params.dist - fade_start) / (fade_end - fade_start))
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
function drawInfiniteGrid(software_renderer, params) {
|
|
989
|
+
const grid_y = params.grid_y ?? 0;
|
|
990
|
+
const cell_size = params.cell_size ?? 1;
|
|
991
|
+
const section_size = params.section_size ?? 10;
|
|
992
|
+
const fade_distance = params.fade_distance ?? 100;
|
|
993
|
+
const fade_strength = params.fade_strength ?? 1.5;
|
|
994
|
+
const grid_color = params.grid_color ?? [0.93, 0.93, 0.93];
|
|
995
|
+
const section_color = params.section_color ?? [0.8, 0.8, 1];
|
|
996
|
+
const gamma_out = params.gamma_out ?? true;
|
|
997
|
+
const camera = params.camera;
|
|
998
|
+
const view = camera.view;
|
|
999
|
+
const proj = camera.proj;
|
|
1000
|
+
const inv_view = mat45.create();
|
|
1001
|
+
mat45.invert(inv_view, view);
|
|
1002
|
+
const cam_pos_x = inv_view[12];
|
|
1003
|
+
const cam_pos_y = inv_view[13];
|
|
1004
|
+
const cam_pos_z = inv_view[14];
|
|
1005
|
+
const vp = mat45.create();
|
|
1006
|
+
mat45.multiply(vp, proj, view);
|
|
1007
|
+
const inv_vp = mat45.create();
|
|
1008
|
+
mat45.invert(inv_vp, vp);
|
|
1009
|
+
for (let y = 0; y < software_renderer.height; y++) {
|
|
1010
|
+
for (let x = 0; x < software_renderer.width; x++) {
|
|
1011
|
+
const ndc_x = x / (software_renderer.width - 1) * 2 - 1;
|
|
1012
|
+
const ndc_y = 1 - y / (software_renderer.height - 1) * 2;
|
|
1013
|
+
const near_point = vec44.fromValues(ndc_x, ndc_y, -1, 1);
|
|
1014
|
+
const far_point = vec44.fromValues(ndc_x, ndc_y, 1, 1);
|
|
1015
|
+
vec44.transformMat4(near_point, near_point, inv_vp);
|
|
1016
|
+
vec44.transformMat4(far_point, far_point, inv_vp);
|
|
1017
|
+
near_point[0] /= near_point[3];
|
|
1018
|
+
near_point[1] /= near_point[3];
|
|
1019
|
+
near_point[2] /= near_point[3];
|
|
1020
|
+
far_point[0] /= far_point[3];
|
|
1021
|
+
far_point[1] /= far_point[3];
|
|
1022
|
+
far_point[2] /= far_point[3];
|
|
1023
|
+
const ray_dir_x = far_point[0] - near_point[0];
|
|
1024
|
+
const ray_dir_y = far_point[1] - near_point[1];
|
|
1025
|
+
const ray_dir_z = far_point[2] - near_point[2];
|
|
1026
|
+
if (Math.abs(ray_dir_y) < 1e-6) continue;
|
|
1027
|
+
const t = (grid_y - near_point[1]) / ray_dir_y;
|
|
1028
|
+
if (t < 0 || t > 1) continue;
|
|
1029
|
+
const world_x = near_point[0] + t * ray_dir_x;
|
|
1030
|
+
const world_z = near_point[2] + t * ray_dir_z;
|
|
1031
|
+
const world_pt = vec44.fromValues(world_x, grid_y, world_z, 1);
|
|
1032
|
+
const clip_pt = vec44.create();
|
|
1033
|
+
vec44.transformMat4(clip_pt, world_pt, vp);
|
|
1034
|
+
const ndc_z = clip_pt[2] / clip_pt[3];
|
|
1035
|
+
const depth_01 = ndc_z * 0.5 + 0.5;
|
|
1036
|
+
const di = y * software_renderer.width + x;
|
|
1037
|
+
if (depth_01 >= software_renderer.depth[di]) continue;
|
|
1038
|
+
const dx = world_x - cam_pos_x;
|
|
1039
|
+
const dy = grid_y - cam_pos_y;
|
|
1040
|
+
const dz = world_z - cam_pos_z;
|
|
1041
|
+
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
1042
|
+
const alpha = computeGridFadeAlpha({
|
|
1043
|
+
dist,
|
|
1044
|
+
fade_distance,
|
|
1045
|
+
fade_strength
|
|
1046
|
+
});
|
|
1047
|
+
if (alpha <= 0) continue;
|
|
1048
|
+
const g1 = computeGridLineIntensity({
|
|
1049
|
+
world_x,
|
|
1050
|
+
world_z,
|
|
1051
|
+
cell_size,
|
|
1052
|
+
dist,
|
|
1053
|
+
screen_width: software_renderer.width,
|
|
1054
|
+
screen_height: software_renderer.height,
|
|
1055
|
+
line_thickness_multiplier: 0.8
|
|
1056
|
+
});
|
|
1057
|
+
const g2 = computeGridLineIntensity({
|
|
1058
|
+
world_x,
|
|
1059
|
+
world_z,
|
|
1060
|
+
cell_size: section_size,
|
|
1061
|
+
dist,
|
|
1062
|
+
screen_width: software_renderer.width,
|
|
1063
|
+
screen_height: software_renderer.height,
|
|
1064
|
+
line_thickness_multiplier: 0.5
|
|
1065
|
+
// Thicker section lines
|
|
1066
|
+
});
|
|
1067
|
+
const grid_strength = Math.max(g1, g2);
|
|
1068
|
+
if (grid_strength <= 0) continue;
|
|
1069
|
+
const final_r = grid_color[0] * (1 - g2) + section_color[0] * g2;
|
|
1070
|
+
const final_g = grid_color[1] * (1 - g2) + section_color[1] * g2;
|
|
1071
|
+
const final_b = grid_color[2] * (1 - g2) + section_color[2] * g2;
|
|
1072
|
+
const final_alpha = grid_strength * alpha;
|
|
1073
|
+
if (final_alpha <= 0.01) continue;
|
|
1074
|
+
let out_r = final_r;
|
|
1075
|
+
let out_g = final_g;
|
|
1076
|
+
let out_b = final_b;
|
|
1077
|
+
if (gamma_out) {
|
|
1078
|
+
out_r = srgbEncodeLinear01(clamp(out_r, 0, 1));
|
|
1079
|
+
out_g = srgbEncodeLinear01(clamp(out_g, 0, 1));
|
|
1080
|
+
out_b = srgbEncodeLinear01(clamp(out_b, 0, 1));
|
|
1081
|
+
} else {
|
|
1082
|
+
out_r = clamp(out_r, 0, 1);
|
|
1083
|
+
out_g = clamp(out_g, 0, 1);
|
|
1084
|
+
out_b = clamp(out_b, 0, 1);
|
|
1085
|
+
}
|
|
1086
|
+
const dst_idx = (y * software_renderer.width + x) * 4;
|
|
1087
|
+
const dst_r = (software_renderer.buffer[dst_idx + 0] ?? 0) / 255;
|
|
1088
|
+
const dst_g = (software_renderer.buffer[dst_idx + 1] ?? 0) / 255;
|
|
1089
|
+
const dst_b = (software_renderer.buffer[dst_idx + 2] ?? 0) / 255;
|
|
1090
|
+
const dst_a = (software_renderer.buffer[dst_idx + 3] ?? 0) / 255;
|
|
1091
|
+
const one_minus_a = 1 - final_alpha;
|
|
1092
|
+
const blend_r = out_r * final_alpha + dst_r * one_minus_a;
|
|
1093
|
+
const blend_g = out_g * final_alpha + dst_g * one_minus_a;
|
|
1094
|
+
const blend_b = out_b * final_alpha + dst_b * one_minus_a;
|
|
1095
|
+
const blend_a = final_alpha + dst_a * one_minus_a;
|
|
1096
|
+
software_renderer.setPixel(
|
|
1097
|
+
x,
|
|
1098
|
+
y,
|
|
1099
|
+
clamp(blend_r, 0, 1) * 255 | 0,
|
|
1100
|
+
clamp(blend_g, 0, 1) * 255 | 0,
|
|
1101
|
+
clamp(blend_b, 0, 1) * 255 | 0,
|
|
1102
|
+
clamp(blend_a, 0, 1) * 255 | 0
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// lib/render/resolveRenderOptions.ts
|
|
1109
|
+
var CAMERA_UP_VALUES = /* @__PURE__ */ new Set(["y+", "y-", "x+", "x-", "z+", "z-"]);
|
|
1110
|
+
function resolveCameraUp(up) {
|
|
1111
|
+
return up != null && CAMERA_UP_VALUES.has(up) ? up : DEFAULT_RENDER_OPTIONS.up;
|
|
1112
|
+
}
|
|
1113
|
+
function resolveCameraRotation(cameraRotation) {
|
|
1114
|
+
if (cameraRotation == null) {
|
|
1115
|
+
return DEFAULT_RENDER_OPTIONS.cameraRotation;
|
|
1116
|
+
}
|
|
1117
|
+
const resolveAxis = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1118
|
+
return {
|
|
1119
|
+
x: resolveAxis(cameraRotation.x),
|
|
1120
|
+
y: resolveAxis(cameraRotation.y),
|
|
1121
|
+
z: resolveAxis(cameraRotation.z)
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
function resolveRenderOptions(options = {}) {
|
|
1125
|
+
const supersampling = typeof options.supersampling === "number" && Number.isFinite(options.supersampling) ? Math.max(1, Math.floor(options.supersampling)) : DEFAULT_RENDER_OPTIONS.supersampling;
|
|
1126
|
+
return {
|
|
1127
|
+
...DEFAULT_RENDER_OPTIONS,
|
|
1128
|
+
...options,
|
|
1129
|
+
supersampling,
|
|
1130
|
+
lightDir: options.lightDir != null ? options.lightDir : DEFAULT_RENDER_OPTIONS.lightDir,
|
|
1131
|
+
camPos: options.camPos !== void 0 ? options.camPos : DEFAULT_RENDER_OPTIONS.camPos,
|
|
1132
|
+
lookAt: options.lookAt !== void 0 ? options.lookAt : DEFAULT_RENDER_OPTIONS.lookAt,
|
|
1133
|
+
up: resolveCameraUp(options.up),
|
|
1134
|
+
cameraRotation: resolveCameraRotation(options.cameraRotation)
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
|
|
809
1138
|
// lib/render/renderDrawCalls.ts
|
|
810
1139
|
function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint8Bitmap) {
|
|
811
1140
|
const options = resolveRenderOptions(optionsInput);
|
|
@@ -817,7 +1146,9 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
|
|
|
817
1146
|
renderHeight,
|
|
818
1147
|
options.fov,
|
|
819
1148
|
options.camPos ?? null,
|
|
820
|
-
options.lookAt ?? null
|
|
1149
|
+
options.lookAt ?? null,
|
|
1150
|
+
options.up,
|
|
1151
|
+
options.cameraRotation ?? null
|
|
821
1152
|
);
|
|
822
1153
|
const renderer = new SoftwareRenderer(renderWidth, renderHeight, imageFactory);
|
|
823
1154
|
let rgb = null;
|
|
@@ -915,6 +1246,16 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
|
|
|
915
1246
|
});
|
|
916
1247
|
}
|
|
917
1248
|
renderGroup(blendDraws);
|
|
1249
|
+
if (options.debugPoints?.length) {
|
|
1250
|
+
drawDebugPoints(
|
|
1251
|
+
renderer,
|
|
1252
|
+
camera,
|
|
1253
|
+
options.debugPoints,
|
|
1254
|
+
options.debugFontSize,
|
|
1255
|
+
options.debugPointColor,
|
|
1256
|
+
options.debugLabelColor
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
918
1259
|
const bitmap = options.supersampling > 1 ? downsampleBitmap(
|
|
919
1260
|
renderer.bitmap,
|
|
920
1261
|
options.width,
|
|
@@ -925,7 +1266,7 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
|
|
|
925
1266
|
}
|
|
926
1267
|
|
|
927
1268
|
// lib/gltf/createInfiniteGrid.ts
|
|
928
|
-
import { mat4 as
|
|
1269
|
+
import { mat4 as mat46 } from "gl-matrix";
|
|
929
1270
|
function createInfiniteGrid(options = {}) {
|
|
930
1271
|
const cellSize = options.cellSize ?? 1;
|
|
931
1272
|
const sectionSize = options.sectionSize ?? 10;
|
|
@@ -949,9 +1290,9 @@ function createInfiniteGrid(options = {}) {
|
|
|
949
1290
|
size
|
|
950
1291
|
]);
|
|
951
1292
|
const indices = new Uint32Array([0, 1, 2, 0, 2, 3]);
|
|
952
|
-
const model =
|
|
1293
|
+
const model = mat46.create();
|
|
953
1294
|
const offset = options.offset ?? {};
|
|
954
|
-
|
|
1295
|
+
mat46.fromTranslation(model, [offset.x ?? 0, offset.y ?? 0, offset.z ?? 0]);
|
|
955
1296
|
return {
|
|
956
1297
|
positions,
|
|
957
1298
|
indices,
|
|
@@ -976,7 +1317,7 @@ function createInfiniteGrid(options = {}) {
|
|
|
976
1317
|
}
|
|
977
1318
|
|
|
978
1319
|
// lib/gltf/createSceneFromGLTF.ts
|
|
979
|
-
import { mat4 as
|
|
1320
|
+
import { mat4 as mat47 } from "gl-matrix";
|
|
980
1321
|
var COMPONENT_INFO = {
|
|
981
1322
|
5120: {
|
|
982
1323
|
name: "BYTE",
|
|
@@ -1174,17 +1515,17 @@ function readIndices(ctx, accessorIndex) {
|
|
|
1174
1515
|
}
|
|
1175
1516
|
function nodeLocalMatrix(node) {
|
|
1176
1517
|
if (node.matrix) {
|
|
1177
|
-
const m2 =
|
|
1518
|
+
const m2 = mat47.create();
|
|
1178
1519
|
for (let i = 0; i < 16; i++) m2[i] = node.matrix[i];
|
|
1179
1520
|
return m2;
|
|
1180
1521
|
}
|
|
1181
1522
|
const translation = node.translation || [0, 0, 0];
|
|
1182
1523
|
const rotation = node.rotation || [0, 0, 0, 1];
|
|
1183
1524
|
const scale = node.scale || [1, 1, 1];
|
|
1184
|
-
const m =
|
|
1185
|
-
const tmp =
|
|
1186
|
-
|
|
1187
|
-
|
|
1525
|
+
const m = mat47.create();
|
|
1526
|
+
const tmp = mat47.create();
|
|
1527
|
+
mat47.fromRotationTranslationScale(tmp, rotation, translation, scale);
|
|
1528
|
+
mat47.copy(m, tmp);
|
|
1188
1529
|
return m;
|
|
1189
1530
|
}
|
|
1190
1531
|
function getMaterial(gltf, textures, images, materialIndex) {
|
|
@@ -1222,8 +1563,8 @@ function createSceneFromGLTF(gltf, resources) {
|
|
|
1222
1563
|
const node = nodes[nodeIndex];
|
|
1223
1564
|
if (!node) return;
|
|
1224
1565
|
const local = nodeLocalMatrix(node);
|
|
1225
|
-
const world =
|
|
1226
|
-
|
|
1566
|
+
const world = mat47.create();
|
|
1567
|
+
mat47.multiply(world, parentMatrix, local);
|
|
1227
1568
|
if (Number.isInteger(node.mesh)) {
|
|
1228
1569
|
const mesh = meshes[node.mesh];
|
|
1229
1570
|
if (mesh) {
|
|
@@ -1261,7 +1602,7 @@ function createSceneFromGLTF(gltf, resources) {
|
|
|
1261
1602
|
traverse(childIndex, world);
|
|
1262
1603
|
}
|
|
1263
1604
|
}
|
|
1264
|
-
const identity =
|
|
1605
|
+
const identity = mat47.create();
|
|
1265
1606
|
for (const rootIndex of scene.nodes || []) {
|
|
1266
1607
|
traverse(rootIndex, identity);
|
|
1267
1608
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poppygl",
|
|
3
3
|
"main": "dist/index.js",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.20",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/tscircuit/poppygl.git"
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"type": "module",
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@biomejs/biome": "^2.2.4",
|
|
23
|
+
"@tscircuit/alphabet": "0.0.23",
|
|
23
24
|
"@types/bun": "^1.2.23",
|
|
24
25
|
"@types/readable-stream": "^4.0.21",
|
|
25
26
|
"looks-same": "^10.0.1",
|