@empty-sekai/sekai-custom-profile-sdk 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +235 -0
- package/LICENSE-EXCEPTION +32 -0
- package/NOTICE +11 -0
- package/README.en.md +456 -0
- package/README.md +458 -0
- package/dist/allium_renderer_wasm.js +14 -0
- package/dist/allium_renderer_wasm.wasm +0 -0
- package/dist/authoring.d.ts +14 -0
- package/dist/authoring.js +34 -0
- package/dist/cache/glyphPersistentCache.d.ts +145 -0
- package/dist/cache/glyphPersistentCache.js +346 -0
- package/dist/cache/indexedDbGlyphRecordStore.d.ts +26 -0
- package/dist/cache/indexedDbGlyphRecordStore.js +214 -0
- package/dist/cache/sessionImageResourceCache.d.ts +39 -0
- package/dist/cache/sessionImageResourceCache.js +156 -0
- package/dist/emscripten.d.ts +11 -0
- package/dist/emscripten.js +1 -0
- package/dist/fontProvider.d.ts +36 -0
- package/dist/fontProvider.js +84 -0
- package/dist/fontSdfAtlas.d.ts +92 -0
- package/dist/fontSdfAtlas.js +296 -0
- package/dist/gpu/browserSemanticResources.d.ts +64 -0
- package/dist/gpu/browserSemanticResources.js +232 -0
- package/dist/gpu/generalTextRenderPlacement.d.ts +8 -0
- package/dist/gpu/generalTextRenderPlacement.js +131 -0
- package/dist/gpu/previewTransformTextureLayout.d.ts +1 -0
- package/dist/gpu/previewTransformTextureLayout.js +12 -0
- package/dist/gpu/semanticCommandGeometry.d.ts +21 -0
- package/dist/gpu/semanticCommandGeometry.js +226 -0
- package/dist/gpu/semanticCommandPlanner.d.ts +131 -0
- package/dist/gpu/semanticCommandPlanner.js +203 -0
- package/dist/gpu/semanticTextGlyphBridge.d.ts +3 -0
- package/dist/gpu/semanticTextGlyphBridge.js +22 -0
- package/dist/gpu/semanticWebglSceneRenderer.d.ts +70 -0
- package/dist/gpu/semanticWebglSceneRenderer.js +211 -0
- package/dist/gpu/slotRanges.d.ts +5 -0
- package/dist/gpu/slotRanges.js +14 -0
- package/dist/gpu/webglSdfAtlasTexture.d.ts +17 -0
- package/dist/gpu/webglSdfAtlasTexture.js +72 -0
- package/dist/gpu/webglSdfGlyphPipeline.d.ts +26 -0
- package/dist/gpu/webglSdfGlyphPipeline.js +267 -0
- package/dist/gpu/webglSemanticCommandExecutor.d.ts +79 -0
- package/dist/gpu/webglSemanticCommandExecutor.js +686 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +8 -0
- package/dist/interaction/numericTextRegions.d.ts +46 -0
- package/dist/interaction/numericTextRegions.js +44 -0
- package/dist/localizationProvider.d.ts +33 -0
- package/dist/localizationProvider.js +74 -0
- package/dist/originPrebuiltSdfAtlasPackage.d.ts +39 -0
- package/dist/originPrebuiltSdfAtlasPackage.js +316 -0
- package/dist/prebuiltSdfAtlas.d.ts +36 -0
- package/dist/prebuiltSdfAtlas.js +261 -0
- package/dist/protocol.d.ts +456 -0
- package/dist/protocol.js +1 -0
- package/dist/renderer.d.ts +218 -0
- package/dist/renderer.js +641 -0
- package/dist/resourceProvider.d.ts +24 -0
- package/dist/resourceProvider.js +41 -0
- package/dist/telemetry/rendererTelemetry.d.ts +158 -0
- package/dist/telemetry/rendererTelemetry.js +232 -0
- package/dist/third-party/freetype/FTL.txt +169 -0
- package/dist/types/atlas.d.ts +90 -0
- package/dist/types/atlas.js +1 -0
- package/dist/types/authoring.d.ts +81 -0
- package/dist/types/authoring.js +1 -0
- package/dist/types/core.d.ts +128 -0
- package/dist/types/core.js +1 -0
- package/dist/types/freeType.d.ts +54 -0
- package/dist/types/freeType.js +13 -0
- package/dist/types/glyph.d.ts +33 -0
- package/dist/types/glyph.js +1 -0
- package/dist/types/layout.d.ts +25 -0
- package/dist/types/layout.js +1 -0
- package/dist/types/text.d.ts +28 -0
- package/dist/types/text.js +1 -0
- package/dist/worker/glyphWorkScheduler.d.ts +29 -0
- package/dist/worker/glyphWorkScheduler.js +107 -0
- package/dist/worker-client.d.ts +114 -0
- package/dist/worker-client.js +413 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +708 -0
- package/package.json +46 -0
- package/src/atlas.rs +652 -0
- package/src/authoring.ts +42 -0
- package/src/authoring_runtime.rs +303 -0
- package/src/cache/glyphPersistentCache.ts +463 -0
- package/src/cache/indexedDbGlyphRecordStore.ts +238 -0
- package/src/cache/sessionImageResourceCache.ts +186 -0
- package/src/edt.rs +97 -0
- package/src/emscripten.ts +17 -0
- package/src/fontProvider.ts +127 -0
- package/src/fontSdfAtlas.ts +431 -0
- package/src/geometry.rs +149 -0
- package/src/glyph_plan.rs +226 -0
- package/src/gpu/browserSemanticResources.ts +294 -0
- package/src/gpu/generalTextRenderPlacement.ts +175 -0
- package/src/gpu/previewTransformTextureLayout.ts +12 -0
- package/src/gpu/semanticCommandGeometry.ts +260 -0
- package/src/gpu/semanticCommandPlanner.ts +280 -0
- package/src/gpu/semanticTextGlyphBridge.ts +35 -0
- package/src/gpu/semanticWebglSceneRenderer.ts +261 -0
- package/src/gpu/slotRanges.ts +15 -0
- package/src/gpu/webglSdfAtlasTexture.ts +69 -0
- package/src/gpu/webglSdfGlyphPipeline.ts +299 -0
- package/src/gpu/webglSemanticCommandExecutor.ts +739 -0
- package/src/index.ts +84 -0
- package/src/interaction/numericTextRegions.ts +80 -0
- package/src/layout.rs +1882 -0
- package/src/lib.rs +1443 -0
- package/src/localizationProvider.ts +113 -0
- package/src/main.rs +55 -0
- package/src/masterdata_runtime.rs +496 -0
- package/src/originPrebuiltSdfAtlasPackage.ts +403 -0
- package/src/prebuiltSdfAtlas.ts +306 -0
- package/src/protocol.ts +150 -0
- package/src/renderer.ts +792 -0
- package/src/resourceProvider.ts +69 -0
- package/src/scene.rs +665 -0
- package/src/telemetry/rendererTelemetry.ts +358 -0
- package/src/types/atlas.ts +80 -0
- package/src/types/authoring.ts +85 -0
- package/src/types/core.ts +110 -0
- package/src/types/freeType.ts +63 -0
- package/src/types/glyph.ts +33 -0
- package/src/types/layout.ts +28 -0
- package/src/types/text.ts +28 -0
- package/src/worker/glyphWorkScheduler.ts +130 -0
- package/src/worker-client.ts +484 -0
- package/src/worker.ts +895 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import type { SdfAtlas } from "../fontSdfAtlas.js";
|
|
2
|
+
import type { WasmLayoutBatch } from "../types/layout.js";
|
|
3
|
+
import { compileSemanticDrawBatches, semanticTextBatchKey, type SemanticDrawBatch } from "./semanticCommandGeometry.js";
|
|
4
|
+
import type { SemanticCommandPlan, SemanticCommandStatePatch, SemanticLayerPatch } from "./semanticCommandPlanner.js";
|
|
5
|
+
import { compileSemanticTextGlyphBatches } from "./semanticTextGlyphBridge.js";
|
|
6
|
+
import { WebglSemanticCommandExecutor, type SemanticGpuMetrics } from "./webglSemanticCommandExecutor.js";
|
|
7
|
+
import { applyNumericRegionRuntimeState, buildNumericTextRegions, type NumericTextRegion } from "../interaction/numericTextRegions.js";
|
|
8
|
+
import type { BrowserImageSource } from "./browserSemanticResources.js";
|
|
9
|
+
import { placeGeneralTextInstances } from "./generalTextRenderPlacement.js";
|
|
10
|
+
import type { GlyphInstance } from "../types/glyph.js";
|
|
11
|
+
import type { SemanticDrawOperation } from "./semanticCommandPlanner.js";
|
|
12
|
+
|
|
13
|
+
export type AuthoredTextHitGeometry = {
|
|
14
|
+
bounds: { x: number; y: number; width: number; height: number };
|
|
15
|
+
quad: [[number, number], [number, number], [number, number], [number, number]];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export class SemanticWebglSceneRenderer {
|
|
19
|
+
private executor: WebglSemanticCommandExecutor;
|
|
20
|
+
private plan: SemanticCommandPlan | null = null;
|
|
21
|
+
private numericRegions: NumericTextRegion[] = [];
|
|
22
|
+
private textHitGeometry = new Map<string, AuthoredTextHitGeometry>();
|
|
23
|
+
private contextLost = false;
|
|
24
|
+
private retainedScene: {
|
|
25
|
+
plan: SemanticCommandPlan;
|
|
26
|
+
atlas: SdfAtlas | null;
|
|
27
|
+
batches: SemanticDrawBatch[];
|
|
28
|
+
imageSources: Map<string, BrowserImageSource>;
|
|
29
|
+
textGlyphBatches: Map<string, Float32Array>;
|
|
30
|
+
} | null = null;
|
|
31
|
+
|
|
32
|
+
constructor(gl: WebGL2RenderingContext) {
|
|
33
|
+
this.executor = new WebglSemanticCommandExecutor(gl);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async setScene(input: {
|
|
37
|
+
plan: SemanticCommandPlan;
|
|
38
|
+
atlas: SdfAtlas | null;
|
|
39
|
+
layout: WasmLayoutBatch;
|
|
40
|
+
imageSources: Map<string, BrowserImageSource>;
|
|
41
|
+
}): Promise<{
|
|
42
|
+
textCommands: number;
|
|
43
|
+
glyphInstances: number;
|
|
44
|
+
atlasUploadBytes: number;
|
|
45
|
+
atlasUploadRects: number;
|
|
46
|
+
}> {
|
|
47
|
+
const operations = input.plan.operations();
|
|
48
|
+
this.plan = input.plan;
|
|
49
|
+
const batches = compileSemanticDrawBatches(operations);
|
|
50
|
+
this.executor.setScene(input.plan, batches, input.imageSources);
|
|
51
|
+
const atlasUpload = input.atlas
|
|
52
|
+
? await this.executor.setSdfAtlas(input.atlas)
|
|
53
|
+
: { bytes: 0, rects: 0 };
|
|
54
|
+
const textOperations = operations.filter((operation) => operation.command.payload.kind === "text");
|
|
55
|
+
const placedInstances = placeGeneralTextInstances(input.layout.instances, textOperations);
|
|
56
|
+
this.textHitGeometry = buildAuthoredTextHitGeometry(textOperations, placedInstances);
|
|
57
|
+
this.numericRegions = textOperations.flatMap((operation) => buildNumericTextRegions(
|
|
58
|
+
operation.command as Parameters<typeof buildNumericTextRegions>[0],
|
|
59
|
+
placedInstances,
|
|
60
|
+
operation.visible && operation.commandVisible,
|
|
61
|
+
));
|
|
62
|
+
const glyphsByCommand = compileSemanticTextGlyphBatches(placedInstances, textOperations);
|
|
63
|
+
const textGlyphBatches = new Map<string, Float32Array>();
|
|
64
|
+
for (const batch of batches.filter((batch) => batch.kind === "text")) {
|
|
65
|
+
const vertices = concatenateFloat32(batch.commandIds.map((commandId) => glyphsByCommand.get(commandId) ?? new Float32Array()));
|
|
66
|
+
const key = semanticTextBatchKey(batch.commandIds);
|
|
67
|
+
textGlyphBatches.set(key, vertices);
|
|
68
|
+
this.executor.setTextGlyphBatch(key, vertices);
|
|
69
|
+
}
|
|
70
|
+
this.retainedScene = {
|
|
71
|
+
plan: input.plan,
|
|
72
|
+
atlas: input.atlas,
|
|
73
|
+
batches,
|
|
74
|
+
imageSources: input.imageSources,
|
|
75
|
+
textGlyphBatches,
|
|
76
|
+
};
|
|
77
|
+
this.contextLost = false;
|
|
78
|
+
return {
|
|
79
|
+
textCommands: textOperations.length,
|
|
80
|
+
glyphInstances: input.layout.instances.length,
|
|
81
|
+
atlasUploadBytes: atlasUpload.bytes,
|
|
82
|
+
atlasUploadRects: atlasUpload.rects,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
applyLayerPatches(patches: SemanticLayerPatch[]) {
|
|
87
|
+
this.assertContextReady();
|
|
88
|
+
return this.executor.applyLayerPatches(patches);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
applyCommandPatches(patches: SemanticCommandStatePatch[]) {
|
|
92
|
+
this.assertContextReady();
|
|
93
|
+
return this.executor.applyCommandPatches(patches);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
applyCoreDelta(delta: { patches: SemanticLayerPatch[]; command_patches: SemanticCommandStatePatch[] }) {
|
|
97
|
+
this.assertContextReady();
|
|
98
|
+
const layer = this.executor.applyLayerPatches(delta.patches);
|
|
99
|
+
const command = this.executor.applyCommandPatches(delta.command_patches);
|
|
100
|
+
return { ...layer, ...command };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
setLayerPreviewTransform(layerId: string, matrix: [number, number, number, number, number, number] | null) {
|
|
104
|
+
this.assertContextReady();
|
|
105
|
+
return this.executor.setLayerPreviewTransform(layerId, matrix);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
draw(): SemanticGpuMetrics {
|
|
109
|
+
this.assertContextReady();
|
|
110
|
+
return this.executor.draw();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
notifyContextLost(): void {
|
|
114
|
+
this.contextLost = true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async restoreContext(gl: WebGL2RenderingContext): Promise<{
|
|
118
|
+
atlasUploadBytes: number;
|
|
119
|
+
atlasUploadRects: number;
|
|
120
|
+
textureUploads: number;
|
|
121
|
+
textureBytes: number;
|
|
122
|
+
}> {
|
|
123
|
+
const retained = this.retainedScene;
|
|
124
|
+
if (!retained) throw new Error("semantic GPU scene is not initialized");
|
|
125
|
+
const previous = this.executor;
|
|
126
|
+
const restored = new WebglSemanticCommandExecutor(gl);
|
|
127
|
+
try {
|
|
128
|
+
restored.setScene(retained.plan, retained.batches, retained.imageSources);
|
|
129
|
+
const atlasUpload = retained.atlas
|
|
130
|
+
? await restored.setSdfAtlas(retained.atlas)
|
|
131
|
+
: { bytes: 0, rects: 0 };
|
|
132
|
+
for (const [key, vertices] of retained.textGlyphBatches) restored.setTextGlyphBatch(key, vertices);
|
|
133
|
+
this.executor = restored;
|
|
134
|
+
this.contextLost = false;
|
|
135
|
+
previous.destroy();
|
|
136
|
+
return {
|
|
137
|
+
atlasUploadBytes: atlasUpload.bytes,
|
|
138
|
+
atlasUploadRects: atlasUpload.rects,
|
|
139
|
+
textureUploads: retained.imageSources.size,
|
|
140
|
+
textureBytes: imageSourceBytes(retained.imageSources.values()),
|
|
141
|
+
};
|
|
142
|
+
} catch (error) {
|
|
143
|
+
restored.destroy();
|
|
144
|
+
this.contextLost = true;
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
interactionRegions(): NumericTextRegion[] {
|
|
150
|
+
const operations = new Map((this.plan?.operations() ?? []).map((operation) => [operation.command.id, operation]));
|
|
151
|
+
return this.numericRegions.map((region) => {
|
|
152
|
+
const operation = operations.get(region.id.split(":numeric:", 1)[0]);
|
|
153
|
+
if (!operation) return region;
|
|
154
|
+
return {
|
|
155
|
+
...applyNumericRegionRuntimeState(region, {
|
|
156
|
+
layerTransform: operation.transform,
|
|
157
|
+
commandTransform: operation.commandTransform,
|
|
158
|
+
}),
|
|
159
|
+
render_mask: operation.visible && operation.commandVisible,
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
authoredTextHitGeometry(): Map<string, AuthoredTextHitGeometry> {
|
|
165
|
+
const operations = this.plan?.operations() ?? [];
|
|
166
|
+
const runtimeByLayer = new Map(operations.map((operation) => [operation.layerId, operation]));
|
|
167
|
+
return new Map([...this.textHitGeometry].map(([layerId, geometry]) => {
|
|
168
|
+
const operation = runtimeByLayer.get(layerId);
|
|
169
|
+
const dx = (operation?.transform.dx ?? 0) + (operation?.commandTransform.dx ?? 0);
|
|
170
|
+
const dy = (operation?.transform.dy ?? 0) + (operation?.commandTransform.dy ?? 0);
|
|
171
|
+
const quad = geometry.quad.map(([x, y]) => [x + dx, y + dy]) as AuthoredTextHitGeometry["quad"];
|
|
172
|
+
return [layerId, {
|
|
173
|
+
bounds: { ...geometry.bounds, x: geometry.bounds.x + dx, y: geometry.bounds.y + dy },
|
|
174
|
+
quad,
|
|
175
|
+
}];
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
destroy(): void {
|
|
180
|
+
this.executor.destroy();
|
|
181
|
+
this.plan = null;
|
|
182
|
+
this.numericRegions = [];
|
|
183
|
+
this.textHitGeometry.clear();
|
|
184
|
+
this.retainedScene = null;
|
|
185
|
+
this.contextLost = false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private assertContextReady(): void {
|
|
189
|
+
if (this.contextLost) throw new Error("WebGL context is lost");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function buildAuthoredTextHitGeometry(
|
|
194
|
+
operations: SemanticDrawOperation[],
|
|
195
|
+
instances: GlyphInstance[],
|
|
196
|
+
): Map<string, AuthoredTextHitGeometry> {
|
|
197
|
+
const output = new Map<string, AuthoredTextHitGeometry>();
|
|
198
|
+
for (const operation of operations) {
|
|
199
|
+
const quads = instances
|
|
200
|
+
.filter((instance) => instance.layerId === operation.command.id)
|
|
201
|
+
.map((instance) => instance.deviceCharQuad[1])
|
|
202
|
+
.filter((quad) => quad.length >= 4);
|
|
203
|
+
const points = quads.flat();
|
|
204
|
+
if (points.length === 0) continue;
|
|
205
|
+
const basis = quads.find((quad) => (
|
|
206
|
+
Math.hypot(quad[1][0] - quad[0][0], quad[1][1] - quad[0][1]) > 1e-4
|
|
207
|
+
&& Math.hypot(quad[3][0] - quad[0][0], quad[3][1] - quad[0][1]) > 1e-4
|
|
208
|
+
));
|
|
209
|
+
if (!basis) continue;
|
|
210
|
+
const ux0 = basis[1][0] - basis[0][0];
|
|
211
|
+
const uy0 = basis[1][1] - basis[0][1];
|
|
212
|
+
const vx0 = basis[3][0] - basis[0][0];
|
|
213
|
+
const vy0 = basis[3][1] - basis[0][1];
|
|
214
|
+
const ul = Math.hypot(ux0, uy0);
|
|
215
|
+
const vl = Math.hypot(vx0, vy0);
|
|
216
|
+
const ux = ux0 / ul;
|
|
217
|
+
const uy = uy0 / ul;
|
|
218
|
+
const vx = vx0 / vl;
|
|
219
|
+
const vy = vy0 / vl;
|
|
220
|
+
const uValues = points.map(([x, y]) => x * ux + y * uy);
|
|
221
|
+
const vValues = points.map(([x, y]) => x * vx + y * vy);
|
|
222
|
+
const minU = Math.min(...uValues);
|
|
223
|
+
const maxU = Math.max(...uValues);
|
|
224
|
+
const minV = Math.min(...vValues);
|
|
225
|
+
const maxV = Math.max(...vValues);
|
|
226
|
+
const point = (u: number, v: number): [number, number] => [ux * u + vx * v, uy * u + vy * v];
|
|
227
|
+
const quad: AuthoredTextHitGeometry["quad"] = [
|
|
228
|
+
point(minU, minV),
|
|
229
|
+
point(maxU, minV),
|
|
230
|
+
point(maxU, maxV),
|
|
231
|
+
point(minU, maxV),
|
|
232
|
+
];
|
|
233
|
+
const xs = quad.map(([x]) => x);
|
|
234
|
+
const ys = quad.map(([, y]) => y);
|
|
235
|
+
const minX = Math.min(...xs);
|
|
236
|
+
const maxX = Math.max(...xs);
|
|
237
|
+
const minY = Math.min(...ys);
|
|
238
|
+
const maxY = Math.max(...ys);
|
|
239
|
+
output.set(operation.layerId, {
|
|
240
|
+
bounds: { x: minX, y: minY, width: maxX - minX, height: maxY - minY },
|
|
241
|
+
quad,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return output;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function concatenateFloat32(values: Float32Array[]): Float32Array {
|
|
248
|
+
const output = new Float32Array(values.reduce((sum, value) => sum + value.length, 0));
|
|
249
|
+
let offset = 0;
|
|
250
|
+
for (const value of values) {
|
|
251
|
+
output.set(value, offset);
|
|
252
|
+
offset += value.length;
|
|
253
|
+
}
|
|
254
|
+
return output;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function imageSourceBytes(values: Iterable<BrowserImageSource>): number {
|
|
258
|
+
let bytes = 0;
|
|
259
|
+
for (const source of values) bytes += Math.max(0, source.width * source.height * 4);
|
|
260
|
+
return bytes;
|
|
261
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type SlotRange = { start: number; end: number };
|
|
2
|
+
|
|
3
|
+
export function contiguousSlotRanges(slots: number[]): SlotRange[] {
|
|
4
|
+
const sorted = [...new Set(slots)].sort((left, right) => left - right);
|
|
5
|
+
const ranges: SlotRange[] = [];
|
|
6
|
+
for (const slot of sorted) {
|
|
7
|
+
const last = ranges.at(-1);
|
|
8
|
+
if (last && slot === last.end) {
|
|
9
|
+
last.end += 1;
|
|
10
|
+
} else {
|
|
11
|
+
ranges.push({ start: slot, end: slot + 1 });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return ranges;
|
|
15
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { SdfAtlas } from "../fontSdfAtlas.js";
|
|
2
|
+
|
|
3
|
+
export class WebglSdfAtlasTexture {
|
|
4
|
+
private textureValue: WebGLTexture;
|
|
5
|
+
private contractId: string | null = null;
|
|
6
|
+
private revisions = new Map<number, number>();
|
|
7
|
+
private allocated = false;
|
|
8
|
+
|
|
9
|
+
constructor(private readonly gl: WebGL2RenderingContext, private readonly maxPages = 6) {
|
|
10
|
+
const texture = gl.createTexture();
|
|
11
|
+
if (!texture) throw new Error("SDF atlas texture allocation failed");
|
|
12
|
+
this.textureValue = texture;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
texture(): WebGLTexture {
|
|
16
|
+
return this.textureValue;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
setAtlas(atlas: SdfAtlas): void {
|
|
20
|
+
if (this.contractId === atlas.contractId) return;
|
|
21
|
+
this.contractId = atlas.contractId;
|
|
22
|
+
this.revisions.clear();
|
|
23
|
+
this.allocated = false;
|
|
24
|
+
this.gl.deleteTexture(this.textureValue);
|
|
25
|
+
const texture = this.gl.createTexture();
|
|
26
|
+
if (!texture) throw new Error("SDF atlas texture reallocation failed");
|
|
27
|
+
this.textureValue = texture;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async uploadUpdates(atlas: SdfAtlas): Promise<{ bytes: number; rects: number }> {
|
|
31
|
+
this.setAtlas(atlas);
|
|
32
|
+
const updates = await atlas.pageUpdates(this.revisions);
|
|
33
|
+
if (updates.length === 0) return { bytes: 0, rects: 0 };
|
|
34
|
+
const gl = this.gl;
|
|
35
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0);
|
|
36
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.textureValue);
|
|
37
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
38
|
+
if (!this.allocated) {
|
|
39
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
40
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
41
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
42
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
43
|
+
gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, gl.R8, atlas.width, atlas.height, this.maxPages);
|
|
44
|
+
this.allocated = true;
|
|
45
|
+
}
|
|
46
|
+
let bytes = 0;
|
|
47
|
+
let rects = 0;
|
|
48
|
+
for (const update of updates) {
|
|
49
|
+
if (update.page >= this.maxPages) throw new Error(`atlas page ${update.page} exceeds GPU hard budget`);
|
|
50
|
+
if (update.fullUpload) {
|
|
51
|
+
gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, update.page, atlas.width, atlas.height, 1, gl.RED, gl.UNSIGNED_BYTE, update.pixels);
|
|
52
|
+
bytes += update.pixels.byteLength;
|
|
53
|
+
rects += 1;
|
|
54
|
+
} else {
|
|
55
|
+
for (const rect of update.dirtyRects) {
|
|
56
|
+
gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, 0, rect.x, rect.y, update.page, rect.width, rect.height, 1, gl.RED, gl.UNSIGNED_BYTE, rect.pixels);
|
|
57
|
+
bytes += rect.pixels.byteLength;
|
|
58
|
+
rects += 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
this.revisions.set(update.page, update.revision);
|
|
62
|
+
}
|
|
63
|
+
return { bytes, rects };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
destroy(): void {
|
|
67
|
+
this.gl.deleteTexture(this.textureValue);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import type { GlyphInstance } from "../types/glyph.js";
|
|
2
|
+
|
|
3
|
+
const CARD_W = 1830;
|
|
4
|
+
const CARD_H = 812;
|
|
5
|
+
const FLOATS_PER_INSTANCE = 36;
|
|
6
|
+
|
|
7
|
+
type GlyphGpuBatch = {
|
|
8
|
+
vao: WebGLVertexArrayObject;
|
|
9
|
+
buffer: WebGLBuffer;
|
|
10
|
+
instances: number;
|
|
11
|
+
bytes: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export class WebglSdfGlyphPipeline {
|
|
15
|
+
private readonly gl: WebGL2RenderingContext;
|
|
16
|
+
private readonly program: WebGLProgram;
|
|
17
|
+
private readonly batches = new Map<string, GlyphGpuBatch>();
|
|
18
|
+
private atlasTexture: WebGLTexture;
|
|
19
|
+
private ownsAtlas = true;
|
|
20
|
+
private geometryBuilds = 0;
|
|
21
|
+
|
|
22
|
+
constructor(gl: WebGL2RenderingContext) {
|
|
23
|
+
this.gl = gl;
|
|
24
|
+
this.program = createProgram(gl, VERTEX_SHADER, FRAGMENT_SHADER);
|
|
25
|
+
const atlas = gl.createTexture();
|
|
26
|
+
if (!atlas) throw new Error("glyph pipeline atlas allocation failed");
|
|
27
|
+
this.atlasTexture = atlas;
|
|
28
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, atlas);
|
|
29
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
30
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
31
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
32
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
33
|
+
gl.texImage3D(gl.TEXTURE_2D_ARRAY, 0, gl.R8, 1, 1, 1, 0, gl.RED, gl.UNSIGNED_BYTE, new Uint8Array([255]));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
setAtlas(texture: WebGLTexture): void {
|
|
37
|
+
if (this.ownsAtlas) this.gl.deleteTexture(this.atlasTexture);
|
|
38
|
+
this.atlasTexture = texture;
|
|
39
|
+
this.ownsAtlas = false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
upload(key: string, vertices: Float32Array): void {
|
|
43
|
+
this.deleteBatch(key);
|
|
44
|
+
if (vertices.length % FLOATS_PER_INSTANCE !== 0) throw new Error(`invalid glyph instance buffer ${key}`);
|
|
45
|
+
const gl = this.gl;
|
|
46
|
+
const vao = gl.createVertexArray();
|
|
47
|
+
const buffer = gl.createBuffer();
|
|
48
|
+
if (!vao || !buffer) throw new Error("glyph pipeline batch allocation failed");
|
|
49
|
+
gl.bindVertexArray(vao);
|
|
50
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
51
|
+
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
|
|
52
|
+
const stride = FLOATS_PER_INSTANCE * 4;
|
|
53
|
+
instanceAttribute(gl, 0, 4, stride, 0);
|
|
54
|
+
instanceAttribute(gl, 1, 4, stride, 4 * 4);
|
|
55
|
+
instanceAttribute(gl, 2, 4, stride, 8 * 4);
|
|
56
|
+
instanceAttribute(gl, 3, 4, stride, 12 * 4);
|
|
57
|
+
instanceAttribute(gl, 4, 4, stride, 16 * 4);
|
|
58
|
+
instanceAttribute(gl, 5, 4, stride, 20 * 4);
|
|
59
|
+
instanceAttribute(gl, 6, 4, stride, 24 * 4);
|
|
60
|
+
instanceAttribute(gl, 7, 4, stride, 28 * 4);
|
|
61
|
+
instanceAttribute(gl, 8, 4, stride, 32 * 4);
|
|
62
|
+
gl.bindVertexArray(null);
|
|
63
|
+
this.batches.set(key, { vao, buffer, instances: vertices.length / FLOATS_PER_INSTANCE, bytes: vertices.byteLength });
|
|
64
|
+
this.geometryBuilds += 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
draw(
|
|
68
|
+
key: string,
|
|
69
|
+
stateTexture: WebGLTexture,
|
|
70
|
+
maskTexture: WebGLTexture,
|
|
71
|
+
stateWidth: number,
|
|
72
|
+
commandMaskTexture: WebGLTexture = maskTexture,
|
|
73
|
+
commandStateTexture: WebGLTexture = stateTexture,
|
|
74
|
+
commandWidth: number = stateWidth,
|
|
75
|
+
previewTransformTexture: WebGLTexture = stateTexture,
|
|
76
|
+
): { drawCalls: number; instances: number; bytes: number } {
|
|
77
|
+
const batch = this.batches.get(key);
|
|
78
|
+
if (!batch || batch.instances === 0) return { drawCalls: 0, instances: 0, bytes: batch?.bytes ?? 0 };
|
|
79
|
+
const gl = this.gl;
|
|
80
|
+
gl.useProgram(this.program);
|
|
81
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
82
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.atlasTexture);
|
|
83
|
+
gl.uniform1i(gl.getUniformLocation(this.program, "u_atlas"), 0);
|
|
84
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
85
|
+
gl.bindTexture(gl.TEXTURE_2D, stateTexture);
|
|
86
|
+
gl.uniform1i(gl.getUniformLocation(this.program, "u_layerState"), 1);
|
|
87
|
+
gl.uniform1f(gl.getUniformLocation(this.program, "u_layerStateWidth"), stateWidth);
|
|
88
|
+
gl.activeTexture(gl.TEXTURE2);
|
|
89
|
+
gl.bindTexture(gl.TEXTURE_2D, maskTexture);
|
|
90
|
+
gl.uniform1i(gl.getUniformLocation(this.program, "u_renderMask"), 2);
|
|
91
|
+
gl.activeTexture(gl.TEXTURE3);
|
|
92
|
+
gl.bindTexture(gl.TEXTURE_2D, commandMaskTexture);
|
|
93
|
+
gl.uniform1i(gl.getUniformLocation(this.program, "u_commandMask"), 3);
|
|
94
|
+
gl.activeTexture(gl.TEXTURE4);
|
|
95
|
+
gl.bindTexture(gl.TEXTURE_2D, commandStateTexture);
|
|
96
|
+
gl.uniform1i(gl.getUniformLocation(this.program, "u_commandState"), 4);
|
|
97
|
+
gl.uniform1f(gl.getUniformLocation(this.program, "u_commandWidth"), commandWidth);
|
|
98
|
+
gl.activeTexture(gl.TEXTURE5);
|
|
99
|
+
gl.bindTexture(gl.TEXTURE_2D, previewTransformTexture);
|
|
100
|
+
gl.uniform1i(gl.getUniformLocation(this.program, "u_previewTransform"), 5);
|
|
101
|
+
gl.bindVertexArray(batch.vao);
|
|
102
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, batch.instances);
|
|
103
|
+
gl.bindVertexArray(null);
|
|
104
|
+
return { drawCalls: 1, instances: batch.instances, bytes: batch.bytes };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
stats(): { geometryBuilds: number; batches: number; bytes: number } {
|
|
108
|
+
return {
|
|
109
|
+
geometryBuilds: this.geometryBuilds,
|
|
110
|
+
batches: this.batches.size,
|
|
111
|
+
bytes: [...this.batches.values()].reduce((sum, batch) => sum + batch.bytes, 0),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
clearBatches(): void {
|
|
116
|
+
for (const key of [...this.batches.keys()]) this.deleteBatch(key);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
destroy(): void {
|
|
120
|
+
this.clearBatches();
|
|
121
|
+
if (this.ownsAtlas) this.gl.deleteTexture(this.atlasTexture);
|
|
122
|
+
this.gl.deleteProgram(this.program);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private deleteBatch(key: string): void {
|
|
126
|
+
const batch = this.batches.get(key);
|
|
127
|
+
if (!batch) return;
|
|
128
|
+
this.gl.deleteBuffer(batch.buffer);
|
|
129
|
+
this.gl.deleteVertexArray(batch.vao);
|
|
130
|
+
this.batches.delete(key);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function buildSdfGlyphInstanceVertices(
|
|
135
|
+
instances: GlyphInstance[],
|
|
136
|
+
layerSlots: ReadonlyMap<string, number>,
|
|
137
|
+
clips: ReadonlyMap<string, [[number, number], [number, number], [number, number], [number, number]]> = new Map(),
|
|
138
|
+
commandSlots: ReadonlyMap<string, number> = layerSlots
|
|
139
|
+
): Float32Array {
|
|
140
|
+
const rows: number[] = [];
|
|
141
|
+
for (const instance of instances) {
|
|
142
|
+
if (!instance.drawable || instance.quad.length < 4) continue;
|
|
143
|
+
const [tl, tr, br, bl] = instance.quad;
|
|
144
|
+
const position = ([x, y]: number[]) => [(x / CARD_W) * 2 - 1, 1 - (y / CARD_H) * 2];
|
|
145
|
+
const clip = clips.get(instance.layerId) ?? [[-1e9, -1e9], [1e9, -1e9], [1e9, 1e9], [-1e9, 1e9]];
|
|
146
|
+
rows.push(
|
|
147
|
+
...position(tl), ...position(tr), ...position(br), ...position(bl),
|
|
148
|
+
tl[2], tl[3], br[2], br[3], ...instance.fill, ...instance.outline,
|
|
149
|
+
instance.shaderFaceScale, instance.shaderFaceBias,
|
|
150
|
+
instance.shaderUnderlayScale, instance.shaderUnderlayBias,
|
|
151
|
+
instance.shaderVertexAlpha, layerSlots.get(instance.layerId) ?? 0,
|
|
152
|
+
instance.atlasPage ?? 0, commandSlots.get(instance.layerId) ?? 0,
|
|
153
|
+
...position(clip[0]), ...position(clip[1]), ...position(clip[2]), ...position(clip[3]),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return new Float32Array(rows);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function instanceAttribute(gl: WebGL2RenderingContext, index: number, size: number, stride: number, offset: number): void {
|
|
160
|
+
gl.enableVertexAttribArray(index);
|
|
161
|
+
gl.vertexAttribPointer(index, size, gl.FLOAT, false, stride, offset);
|
|
162
|
+
gl.vertexAttribDivisor(index, 1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function createProgram(gl: WebGL2RenderingContext, vertexSource: string, fragmentSource: string): WebGLProgram {
|
|
166
|
+
const compile = (type: number, source: string) => {
|
|
167
|
+
const shader = gl.createShader(type);
|
|
168
|
+
if (!shader) throw new Error("glyph shader allocation failed");
|
|
169
|
+
gl.shaderSource(shader, source);
|
|
170
|
+
gl.compileShader(shader);
|
|
171
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(shader) ?? "glyph shader compile failed");
|
|
172
|
+
return shader;
|
|
173
|
+
};
|
|
174
|
+
const vertex = compile(gl.VERTEX_SHADER, vertexSource);
|
|
175
|
+
const fragment = compile(gl.FRAGMENT_SHADER, fragmentSource);
|
|
176
|
+
const program = gl.createProgram();
|
|
177
|
+
if (!program) throw new Error("glyph program allocation failed");
|
|
178
|
+
gl.attachShader(program, vertex);
|
|
179
|
+
gl.attachShader(program, fragment);
|
|
180
|
+
gl.linkProgram(program);
|
|
181
|
+
gl.deleteShader(vertex);
|
|
182
|
+
gl.deleteShader(fragment);
|
|
183
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(program) ?? "glyph program link failed");
|
|
184
|
+
return program;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const VERTEX_SHADER = `#version 300 es
|
|
188
|
+
precision highp float;
|
|
189
|
+
layout(location=0) in vec4 a_pos01;
|
|
190
|
+
layout(location=1) in vec4 a_pos23;
|
|
191
|
+
layout(location=2) in vec4 a_uvRect;
|
|
192
|
+
layout(location=3) in vec4 a_color;
|
|
193
|
+
layout(location=4) in vec4 a_outline;
|
|
194
|
+
layout(location=5) in vec4 a_sdfParams;
|
|
195
|
+
layout(location=6) in vec4 a_instanceMeta;
|
|
196
|
+
layout(location=7) in vec4 a_clip01;
|
|
197
|
+
layout(location=8) in vec4 a_clip23;
|
|
198
|
+
uniform sampler2D u_layerState;
|
|
199
|
+
uniform highp usampler2D u_renderMask;
|
|
200
|
+
uniform float u_layerStateWidth;
|
|
201
|
+
uniform highp usampler2D u_commandMask;
|
|
202
|
+
uniform sampler2D u_commandState;
|
|
203
|
+
uniform float u_commandWidth;
|
|
204
|
+
uniform sampler2D u_previewTransform;
|
|
205
|
+
out vec2 v_uv;
|
|
206
|
+
out vec4 v_color;
|
|
207
|
+
out vec4 v_outline;
|
|
208
|
+
out float v_faceScale;
|
|
209
|
+
out float v_faceBias;
|
|
210
|
+
out float v_underlayScale;
|
|
211
|
+
out float v_underlayBias;
|
|
212
|
+
out float v_vertexAlpha;
|
|
213
|
+
flat out float v_atlasPage;
|
|
214
|
+
out vec2 v_point;
|
|
215
|
+
out vec4 v_clip01;
|
|
216
|
+
out vec4 v_clip23;
|
|
217
|
+
const int CORNER_IDS[6] = int[6](0, 1, 3, 3, 1, 2);
|
|
218
|
+
void main() {
|
|
219
|
+
vec2 positions[4] = vec2[4](a_pos01.xy, a_pos01.zw, a_pos23.xy, a_pos23.zw);
|
|
220
|
+
vec2 uvs[4] = vec2[4](a_uvRect.xy, vec2(a_uvRect.z, a_uvRect.y), a_uvRect.zw, vec2(a_uvRect.x, a_uvRect.w));
|
|
221
|
+
int corner = CORNER_IDS[gl_VertexID];
|
|
222
|
+
float layerSlot = a_instanceMeta.y;
|
|
223
|
+
float stateU = (layerSlot + 0.5) / max(u_layerStateWidth, 1.0);
|
|
224
|
+
vec2 state = texture(u_layerState, vec2(stateU, 0.5)).rg;
|
|
225
|
+
uint mask = texelFetch(u_renderMask, ivec2(int(layerSlot), 0), 0).r;
|
|
226
|
+
uint commandMask = texelFetch(u_commandMask, ivec2(int(a_instanceMeta.w), 0), 0).r;
|
|
227
|
+
vec2 commandState = texelFetch(u_commandState, ivec2(int(a_instanceMeta.w), 0), 0).rg;
|
|
228
|
+
vec2 totalState = state + commandState;
|
|
229
|
+
vec4 preview0 = texelFetch(u_previewTransform, ivec2(int(layerSlot), 0), 0);
|
|
230
|
+
vec4 preview1 = texelFetch(u_previewTransform, ivec2(int(layerSlot), 1), 0);
|
|
231
|
+
vec2 pixelPosition = vec2(
|
|
232
|
+
(positions[corner].x + 1.0) * ${CARD_W / 2}.0,
|
|
233
|
+
(1.0 - positions[corner].y) * ${CARD_H / 2}.0
|
|
234
|
+
) + totalState;
|
|
235
|
+
vec2 previewedPosition = vec2(
|
|
236
|
+
dot(preview0.xy, pixelPosition) + preview0.z,
|
|
237
|
+
dot(preview1.xy, pixelPosition) + preview1.z
|
|
238
|
+
);
|
|
239
|
+
vec2 position = vec2(
|
|
240
|
+
previewedPosition.x * ${2 / CARD_W} - 1.0,
|
|
241
|
+
1.0 - previewedPosition.y * ${2 / CARD_H}
|
|
242
|
+
);
|
|
243
|
+
vec2 clipPoints[4] = vec2[4](a_clip01.xy, a_clip01.zw, a_clip23.xy, a_clip23.zw);
|
|
244
|
+
for (int index = 0; index < 4; index += 1) {
|
|
245
|
+
vec2 clipPixel = vec2((clipPoints[index].x + 1.0) * ${CARD_W / 2}.0, (1.0 - clipPoints[index].y) * ${CARD_H / 2}.0) + state;
|
|
246
|
+
vec2 transformed = vec2(dot(preview0.xy, clipPixel) + preview0.z, dot(preview1.xy, clipPixel) + preview1.z);
|
|
247
|
+
clipPoints[index] = vec2(transformed.x * ${2 / CARD_W} - 1.0, 1.0 - transformed.y * ${2 / CARD_H});
|
|
248
|
+
}
|
|
249
|
+
if (mask == 0u || commandMask == 0u) position = vec2(2.0);
|
|
250
|
+
gl_Position = vec4(position, 0.0, 1.0);
|
|
251
|
+
v_uv = uvs[corner];
|
|
252
|
+
v_color = a_color;
|
|
253
|
+
v_outline = a_outline;
|
|
254
|
+
v_faceScale = max(a_sdfParams.x, 0.0001);
|
|
255
|
+
v_faceBias = a_sdfParams.y;
|
|
256
|
+
v_underlayScale = max(a_sdfParams.z, 0.0001);
|
|
257
|
+
v_underlayBias = a_sdfParams.w;
|
|
258
|
+
v_vertexAlpha = clamp(a_instanceMeta.x, 0.0, 1.0);
|
|
259
|
+
v_atlasPage = a_instanceMeta.z;
|
|
260
|
+
v_point = position;
|
|
261
|
+
v_clip01 = vec4(clipPoints[0], clipPoints[1]);
|
|
262
|
+
v_clip23 = vec4(clipPoints[2], clipPoints[3]);
|
|
263
|
+
}`;
|
|
264
|
+
|
|
265
|
+
const FRAGMENT_SHADER = `#version 300 es
|
|
266
|
+
precision highp float;
|
|
267
|
+
uniform highp sampler2DArray u_atlas;
|
|
268
|
+
in vec2 v_uv;
|
|
269
|
+
in vec4 v_color;
|
|
270
|
+
in vec4 v_outline;
|
|
271
|
+
in float v_faceScale;
|
|
272
|
+
in float v_faceBias;
|
|
273
|
+
in float v_underlayScale;
|
|
274
|
+
in float v_underlayBias;
|
|
275
|
+
in float v_vertexAlpha;
|
|
276
|
+
flat in float v_atlasPage;
|
|
277
|
+
in vec2 v_point;
|
|
278
|
+
in vec4 v_clip01;
|
|
279
|
+
in vec4 v_clip23;
|
|
280
|
+
out vec4 outColor;
|
|
281
|
+
float cross2(vec2 a, vec2 b) { return a.x * b.y - a.y * b.x; }
|
|
282
|
+
bool insideClip() {
|
|
283
|
+
vec2 p[4] = vec2[4](v_clip01.xy, v_clip01.zw, v_clip23.xy, v_clip23.zw);
|
|
284
|
+
float c0 = cross2(p[1] - p[0], v_point - p[0]);
|
|
285
|
+
float c1 = cross2(p[2] - p[1], v_point - p[1]);
|
|
286
|
+
float c2 = cross2(p[3] - p[2], v_point - p[2]);
|
|
287
|
+
float c3 = cross2(p[0] - p[3], v_point - p[3]);
|
|
288
|
+
return (c0 >= 0.0 && c1 >= 0.0 && c2 >= 0.0 && c3 >= 0.0) || (c0 <= 0.0 && c1 <= 0.0 && c2 <= 0.0 && c3 <= 0.0);
|
|
289
|
+
}
|
|
290
|
+
void main() {
|
|
291
|
+
if (!insideClip()) discard;
|
|
292
|
+
float sdf = texture(u_atlas, vec3(v_uv, v_atlasPage)).r;
|
|
293
|
+
float faceT = clamp(sdf * v_faceScale - v_faceBias, 0.0, 1.0);
|
|
294
|
+
float underlayT = clamp(sdf * v_underlayScale - v_underlayBias, 0.0, 1.0) * clamp(sdf * 12.5, 0.0, 1.0);
|
|
295
|
+
vec4 face = vec4(v_color.rgb * v_color.a, v_color.a);
|
|
296
|
+
vec4 outline = vec4(v_outline.rgb * v_outline.a, v_outline.a);
|
|
297
|
+
float oneMinusFace = 1.0 - face.a * faceT;
|
|
298
|
+
outColor = (face * faceT + outline * underlayT * oneMinusFace) * v_vertexAlpha;
|
|
299
|
+
}`;
|