Skip to content

Commit 4cd1084

Browse files
authored
fix: more efficient draw shape (#355)
1 parent 71f3da3 commit 4cd1084

29 files changed

Lines changed: 2197 additions & 463 deletions

ADRs/SDF rendering

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ TO know if you are inside or outside we can store red channe lwit hnegative or p
77
Switch from using raycasting/widning number and dissallow hoels in a shape UNLESS we do it intentionally by boolean oepration (so inner curve direciton is reversed) or in fonts (so their inner cuvre direciton is reversE).
88

99
This way we can use jsut tangent fast oepraiton in FRAGMENT SHADER to determine where pixels lives.
10+
11+
And there was one more attempt with Claude Opus 4.7 with Adpative Thinking. Still failed to make SDF good enough.

ADRs/custom programs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
I've tried to use failing program as any other. But here is the issue, if a program fails then the whole encoder fails, so all assumptions like "sdf should be generated" don't hold anymore.
2-
It complicates whole renderign logic on each step(by handling that edge case).
2+
It complicates whole rendering logic on each step(by handling that edge case).
33

44
Because of that, we switch approach, firstly let's validate if program works, if not, use alternative program, if works, then fine. This way we can awlays be sure that encoder won't fail because of custom programs.

integration-tests/index.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ async function test() {
8989
sharedTextEffects.style.display = 'block'
9090
sharedTextEffects.checked = selectedAsset.is_sdf_shared
9191
fontSizeInput.value = selectedAsset.typo_props.font_size.toString()
92-
console.log(selectedAsset.typo_props.font_size)
9392
} else {
9493
sharedTextEffects.style.display = 'none'
9594
}
@@ -158,7 +157,7 @@ async function test() {
158157
),
159158
}
160159

161-
creator.setSnapshot(snaitizedLastSnapshot, true)
160+
creator.setSnapshot(snaitizedLastSnapshot, { produceSnapshot: false, addHistoryEntry: true })
162161

163162
const resetProjectBtn = document.querySelector<HTMLInputElement>('#reset-project')!
164163
resetProjectBtn.addEventListener('click', () => {
@@ -197,7 +196,7 @@ async function test() {
197196
})),
198197
}
199198

200-
creator.setSnapshot(snapshot, true)
199+
creator.setSnapshot(snapshot, { produceSnapshot: false, addHistoryEntry: true })
201200
startProjectInputFromImages.value = '' // reset input value to allow re-uploading the same file
202201
})
203202

@@ -208,14 +207,14 @@ async function test() {
208207
undoBtn.addEventListener('click', () => {
209208
currentHistoryIndex = Math.max(0, currentHistoryIndex - 1)
210209
const snapshot = assetsUpdatesHistory[currentHistoryIndex]
211-
creator.setSnapshot(snapshot, false)
210+
creator.setSnapshot(snapshot, { produceSnapshot: false, addHistoryEntry: true })
212211
setLastSnapshot(snapshot)
213212
})
214213

215214
redoBtn.addEventListener('click', () => {
216215
currentHistoryIndex = Math.min(assetsUpdatesHistory.length - 1, currentHistoryIndex + 1)
217216
const snapshot = assetsUpdatesHistory[currentHistoryIndex]
218-
creator.setSnapshot(snapshot, false)
217+
creator.setSnapshot(snapshot, { produceSnapshot: false, addHistoryEntry: true })
219218
setLastSnapshot(snapshot)
220219
})
221220

@@ -241,7 +240,7 @@ async function test() {
241240
...getLastSnapshot(),
242241
assets: newAssets,
243242
},
244-
true
243+
{ produceSnapshot: true, addHistoryEntry: true }
245244
)
246245
})
247246

@@ -290,7 +289,7 @@ async function test() {
290289
width,
291290
height,
292291
},
293-
true
292+
{ produceSnapshot: true, addHistoryEntry: true }
294293
)
295294
})
296295

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
const STRAIGHT_LINE_THRESHOLD = 1e10;
2+
const EPSILON = 1e-10;
3+
const PI = 3.141592653589793;
4+
const FWIDTH_VALID_LIMIT = 100;
5+
// Shapes share a single SDF texture. Pixels not covered by any shape are
6+
// initialized to -3.402823466e+38 before per-shape SDF values are written.
7+
// This creates extremely large distance derivatives at the boundary between
8+
// real shape SDF values and the default background value, so we ignore
9+
// derivatives larger than FWIDTH_VALID_LIMIT.
10+
11+
const UNIFORM_T_SAMPLING = 4.0;
12+
13+
struct CubicBezier {
14+
p0: vec2f,
15+
p1: vec2f,
16+
p2: vec2f,
17+
p3: vec2f,
18+
};
19+
20+
fn bezier_point(curve: CubicBezier, t: f32) -> vec2f {
21+
let t2 = t * t;
22+
let t3 = t2 * t;
23+
let one_minus_t = 1.0 - t;
24+
let one_minus_t2 = one_minus_t * one_minus_t;
25+
let one_minus_t3 = one_minus_t2 * one_minus_t;
26+
27+
return curve.p0 * one_minus_t3 +
28+
3.0 * curve.p1 * t * one_minus_t2 +
29+
3.0 * curve.p2 * t2 * one_minus_t +
30+
curve.p3 * t3;
31+
}
32+
33+
34+
struct Vertex {
35+
@location(0) position: vec4f,
36+
};
37+
38+
// @group(0) @binding(0) var ourSampler: sampler;
39+
@group(0) @binding(1) var texture: texture_2d<f32>;
40+
// @group(0) @binding(1) var texture: texture_storage_2d<rgba32float, read>;
41+
@group(0) @binding(2) var<uniform> camera_projection: mat4x4f;
42+
@group(0) @binding(3) var<storage, read> curves: array<vec2f>;
43+
// @group(0) @binding(4) var<storage, read> uniform_t: array<f32>;
44+
// @group(0) @binding(5) var ourSampler: sampler;
45+
// consider witchign to uniform if possible
46+
47+
struct VSOutput {
48+
@builtin(position) position: vec4f,
49+
@location(0) uv: vec2f,
50+
@location(1) norm_uv: vec2f,
51+
};
52+
53+
@vertex fn vs(vert: Vertex) -> VSOutput {
54+
let size = textureDimensions(texture);
55+
return VSOutput(
56+
camera_projection * vec4f(vert.position.xy, 0.0, 1.0),
57+
vert.position.zw * (vec2f(size) + vec2f(0)),
58+
vert.position.zw,
59+
);
60+
}
61+
62+
const BILINEAR_T_THRESHOLD = 1;
63+
const BILINEAR_T_THRESHOLD_POS = 2;
64+
// all texels which has diff with nearest texel < BILINEAR_T_THRESHOLD
65+
// will be included in bilinear interpolation.
66+
// It helps avoid interpolating t from totally different places
67+
68+
struct Sample {
69+
t: f32,
70+
sign: f32,
71+
};
72+
73+
fn getSample(pos: vec2f) -> Sample {
74+
let floor_pos = floor(pos - 0.5);
75+
let fract_pos = pos - 0.5 - floor_pos;
76+
77+
let max_coord = vec2i(vec2i(textureDimensions(texture)) - vec2i(1, 1));
78+
79+
let p00 = vec2u(clamp(vec2i(floor_pos ), vec2i(0, 0), max_coord));
80+
let p10 = vec2u(clamp(vec2i(floor_pos + vec2f(1.0, 0.0)), vec2i(0, 0), max_coord));
81+
let p01 = vec2u(clamp(vec2i(floor_pos + vec2f(0.0, 1.0)), vec2i(0, 0), max_coord));
82+
let p11 = vec2u(clamp(vec2i(floor_pos + vec2f(1.0, 1.0)), vec2i(0, 0), max_coord));
83+
84+
// textureSample(ourTexture, ourSampler, fsInput.texcoord);
85+
let raw_c00 = textureLoad(texture, p00, 0).r;
86+
let raw_c10 = textureLoad(texture, p10, 0).r;
87+
let raw_c01 = textureLoad(texture, p01, 0).r;
88+
let raw_c11 = textureLoad(texture, p11, 0).r;
89+
90+
let c00 = abs(raw_c00) - 1;
91+
let c10 = abs(raw_c10) - 1;
92+
let c01 = abs(raw_c01) - 1;
93+
let c11 = abs(raw_c11) - 1;
94+
95+
let g10 = select(c00, c10, abs(c10 - c00) < BILINEAR_T_THRESHOLD);
96+
let g01 = select(c00, c01, abs(c01 - c00) < BILINEAR_T_THRESHOLD);
97+
let g11 = select(c00, c11, abs(c11 - c00) < BILINEAR_T_THRESHOLD);
98+
99+
let top = mix(c00, g10, fract_pos.x);
100+
let bottom = mix(g01, g11, fract_pos.x);
101+
let final_t = mix(top, bottom, fract_pos.y);
102+
103+
// Pick sign from the nearest texel to pos (not always p00)
104+
let nearest_raw = select(
105+
select(raw_c00, raw_c01, fract_pos.y >= 0.5),
106+
select(raw_c10, raw_c11, fract_pos.y >= 0.5),
107+
fract_pos.x >= 0.5
108+
);
109+
110+
return Sample(final_t, sign(nearest_raw));
111+
}
112+
113+
fn g_to_bezier_pos(global_t: f32) -> vec2f {
114+
let idx = u32(global_t);
115+
let local_t = fract(global_t);
116+
let curve = CubicBezier(
117+
curves[idx * 4 + 0],
118+
curves[idx * 4 + 1],
119+
curves[idx * 4 + 2],
120+
curves[idx * 4 + 3]
121+
);
122+
123+
let is_straight_line = curve.p1.x > STRAIGHT_LINE_THRESHOLD;
124+
if (is_straight_line) {
125+
return mix(curve.p0, curve.p3, local_t);
126+
}
127+
128+
return bezier_point(curve, local_t);
129+
}
130+
131+
@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f {
132+
let sample = getSample(vsOut.uv);
133+
let curve_pos = g_to_bezier_pos(sample.t);
134+
let distance = length(curve_pos - vsOut.uv);
135+
let signed_distance = distance * sample.sign;
136+
137+
${TEST}
138+
139+
let dist_derivative = fwidth(signed_distance);
140+
141+
let safe_dist_derivative = select(0.0, dist_derivative, dist_derivative <= FWIDTH_VALID_LIMIT); // if too large -> 0
142+
let alpha_smooth_factor = safe_dist_derivative * 1;
143+
// let alpha_smooth_factor = max(safe_dist_derivative * 1, EPSILON);
144+
145+
let inner_alpha = smoothstep(u.dist_start - alpha_smooth_factor, u.dist_start + alpha_smooth_factor, signed_distance);
146+
let outer_alpha = smoothstep(u.dist_end - alpha_smooth_factor, u.dist_end + alpha_smooth_factor, signed_distance);
147+
let alpha = outer_alpha - inner_alpha;
148+
let color = getColor(vec4f(signed_distance, 0, 0, 1), vsOut.uv, vsOut.norm_uv);
149+
let result = vec4f(color.rgb, color.a * alpha);
150+
151+
// if (result.a < EPSILON) {
152+
// return vec4f(0.5);
153+
// }
154+
155+
return result;
156+
157+
// let stroke_factor = select(0.5, 0.0, sdf.g > 1.0);
158+
// color = vec4f(0, sdf.g % 1, 0, 1.0);
159+
// color = vec4f(0, 0, sdf.b / (2 * PI), 1.0);
160+
// color = vec4f(sdf.r / 100.0, sdf.g % 1, sdf.b / (2 * PI), 1.0);
161+
// color = select(vec4f(0.5, 0, 0, 1), vec4f(0, 0, 0.5, 1), u32(sdf.r / 20.0) % 2 == 0);
162+
}

0 commit comments

Comments
 (0)