Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

blank: 3D normal backface culling + front-face-only edges

- Compute 3D face normals and dot with view direction for robust culling
(works regardless of vertex winding or hinge transform)
- Edges only render if they touch at least one front-facing face
- Keyboard only visible when base top face is front-facing AND lid is behind
- Removes all z-fighting from back-face bleed-through

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+50 -27
+50 -27
system/public/aesthetic.computer/disks/blank.mjs
··· 230 230 // Collect all drawable quads with z-depth 231 231 const drawList = []; 232 232 233 - // Base + lid faces 234 - // Only add front-facing faces (proper backface culling — no both-winding hack) 235 - const addFaces = (proj, color, tag) => { 236 - for (const [a, b, c, d] of faceQuads) { 237 - const e1x = proj[b][0] - proj[a][0], e1y = proj[b][1] - proj[a][1]; 238 - const e2x = proj[d][0] - proj[a][0], e2y = proj[d][1] - proj[a][1]; 239 - if (e1x * e2y - e1y * e2x >= 0) continue; // back-facing → skip 233 + // 3D face normal backface culling — works regardless of vertex winding 234 + // Compute view direction (camera looks toward +z in world, before rotations) 235 + // After ay (Y-rot) and ax (X-rot), the view direction is: 236 + const viewDirX = sin(ay) * cos(ax); 237 + const viewDirY = sin(ax); 238 + const viewDirZ = cos(ay) * cos(ax); 239 + 240 + const addFaces = (verts3d, proj, color, tag) => { 241 + const frontFaces = new Set(); 242 + for (let fi = 0; fi < faceQuads.length; fi++) { 243 + const [a, b, c, d] = faceQuads[fi]; 244 + // 3D face normal via cross product of two edges 245 + const e1 = [verts3d[b][0] - verts3d[a][0], verts3d[b][1] - verts3d[a][1], verts3d[b][2] - verts3d[a][2]]; 246 + const e2 = [verts3d[d][0] - verts3d[a][0], verts3d[d][1] - verts3d[a][1], verts3d[d][2] - verts3d[a][2]]; 247 + const nx = e1[1] * e2[2] - e1[2] * e2[1]; 248 + const ny = e1[2] * e2[0] - e1[0] * e2[2]; 249 + const nz = e1[0] * e2[1] - e1[1] * e2[0]; 250 + // Dot with view direction — positive = facing camera 251 + const dot = nx * viewDirX + ny * viewDirY + nz * viewDirZ; 252 + if (dot <= 0) continue; // back-facing 253 + frontFaces.add(fi); 240 254 const z = (proj[a][2] + proj[b][2] + proj[c][2] + proj[d][2]) / 4; 241 255 drawList.push({ z, type: "face", proj, verts: [a, b, c, d], color, tag }); 242 256 } 257 + return frontFaces; 243 258 }; 244 - // Lid winding flips due to hinge transform — use reversed face quads 245 - const faceQuadsFlipped = faceQuads.map(([a, b, c, d]) => [d, c, b, a]); 246 - const addFacesFlipped = (proj, color, tag) => { 247 - for (const [a, b, c, d] of faceQuadsFlipped) { 248 - const e1x = proj[b][0] - proj[a][0], e1y = proj[b][1] - proj[a][1]; 249 - const e2x = proj[d][0] - proj[a][0], e2y = proj[d][1] - proj[a][1]; 250 - if (e1x * e2y - e1y * e2x >= 0) continue; 251 - const z = (proj[a][2] + proj[b][2] + proj[c][2] + proj[d][2]) / 4; 252 - drawList.push({ z, type: "face", proj, verts: [a, b, c, d], color, tag }); 253 - } 254 - }; 255 - addFaces(projBase, baseColor, "base"); 256 - addFacesFlipped(projLid, lidColor, "lid"); 259 + 260 + const baseFrontFaces = addFaces(base, projBase, baseColor, "base"); 261 + const lidFrontFaces = addFaces(lid, projLid, lidColor, "lid"); 257 262 258 263 // Keyboard keys (on base top face, y = -0.001 just above y=0) 259 264 const kbInset = 0.18; ··· 333 338 }); 334 339 } 335 340 336 - // Wireframe edges — white, semi-transparent, blinky 337 - const addEdges = (proj) => { 341 + // Wireframe edges — only for edges that touch at least one front-facing face 342 + // Map each edge to which faces it belongs to 343 + const edgeFaceMap = {}; // "a-b" → [faceIdx, ...] 344 + faceQuads.forEach(([a, b, c, d], fi) => { 345 + const edges = [[a,b],[b,c],[c,d],[d,a]]; 346 + // For our quads: edges are [a,b],[b,c=next],[c,d],[d,a] 347 + // But faceQuads use 4 verts, edges are between consecutive + wrap 348 + [[a,b],[b,c],[c,d],[d,a]].forEach(([ea, eb]) => { 349 + const key = min(ea,eb) + "-" + max(ea,eb); 350 + (edgeFaceMap[key] = edgeFaceMap[key] || []).push(fi); 351 + }); 352 + }); 353 + 354 + const addEdges = (proj, frontFaces) => { 338 355 halfEdges.forEach(([a, b]) => { 356 + const key = min(a,b) + "-" + max(a,b); 357 + const faces = edgeFaceMap[key] || []; 358 + // Only draw if at least one adjacent face is front-facing 359 + if (!faces.some(fi => frontFaces.has(fi))) return; 339 360 const z = (proj[a][2] + proj[b][2]) / 2; 340 361 drawList.push({ z, type: "edge", proj, a, b }); 341 362 }); 342 363 }; 343 - addEdges(projBase); 344 - addEdges(projLid); 364 + addEdges(projBase, baseFrontFaces); 365 + addEdges(projLid, lidFrontFaces); 345 366 346 367 // Sort back-to-front (highest z = farthest = draw first) 347 368 drawList.sort((a, b) => b.z - a.z); ··· 362 383 } 363 384 } 364 385 365 - // ⌨️ Keyboard keys — only draw when lid isn't occluding the base 366 - const baseAvgZ = projBase.reduce((s, v) => s + v[2], 0) / projBase.length; 386 + // ⌨️ Keyboard keys — only draw when base top face (faceQuads[0]) is front-facing 387 + // and lid isn't in front of base 388 + const baseTopVisible = baseFrontFaces.has(0); 367 389 const lidAvgZ = projLid.reduce((s, v) => s + v[2], 0) / projLid.length; 368 - const kbNotOccluded = lidAvgZ > baseAvgZ; // lid is farther = behind base 390 + const baseAvgZ = projBase.reduce((s, v) => s + v[2], 0) / projBase.length; 391 + const kbNotOccluded = baseTopVisible && lidAvgZ >= baseAvgZ; 369 392 if (kbVisible && kbNotOccluded) { 370 393 for (const key of kbKeys) { 371 394 const [[x0, y0], [x1, y1], [x2, y2], [x3, y3]] = key.pts;