Key Takeaways
- Intraoral scanners produce STL files with 2-4 million triangles. Rendering these in the browser requires a level-of-detail pipeline, not just a bigger GPU.
- Binary STL parsing in a streaming Node.js service avoids loading full meshes into memory, which matters when handling concurrent uploads from multiple clinics.
- Quadric Edge Collapse decimation can reduce triangle counts by 85-95% while keeping Hausdorff distance under 0.05mm -- well within clinical tolerance for most review tasks.
- HIPAA-compliant scan storage requires more than encryption at rest. We used presigned S3 URLs with short TTLs and client-side-only rendering to keep PHI off the server after the initial optimization pass.
- The collaborative annotation layer turned out to be more valuable to clinicians than the 3D rendering itself. Build the communication tool first.
The Problem with Physical Impressions
Dental practices that adopt intraoral scanners typically solve the scanning problem but not the workflow problem. The scanner produces an STL file. Then what? In most practices, that file gets exported to a USB drive, uploaded through the scanner vendor's desktop software, and emailed to a lab. Files get lost between handoffs. Labs receive the wrong version. Clinicians can't review scans without launching proprietary software that only runs on one workstation.
The underlying challenge is straightforward: intraoral STL files are large, dense meshes that need to be viewable in a browser, storable in a HIPAA-compliant way, and routable to downstream lab workflows without manual file shuffling. This post covers the architecture we built to handle that pipeline.
Why a Browser-Based Viewer
The alternative to a browser-based viewer is a native desktop application. We considered this and rejected it for two reasons. First, dental clinic IT environments are heterogeneous -- a mix of Windows 10 and 11 machines, varying GPU capabilities, and restrictive group policies that make desktop installs painful. Second, the lab technicians reviewing scans work in a different organization entirely, so distributing and maintaining a native app across organizational boundaries adds ongoing friction. A browser-based viewer with WebGL rendering sidesteps both problems.
The trade-off is performance. WebGL gives you less control over the rendering pipeline than a native OpenGL or Vulkan application. Clinic workstations typically run integrated Intel UHD 630 GPUs, which can handle maybe 300,000-500,000 triangles at 60fps in a Three.js scene. Full-arch dental scans can have 4 million triangles. That gap defined most of our technical decisions.
Architecture and Tech Stack
The system has three layers: an upload and processing pipeline that handles STL ingestion and mesh optimization, a storage layer for HIPAA-compliant scan persistence, and a browser-based viewer for rendering and annotation. Here is what we used and why.
- Frontend rendering: Three.js (r152). We evaluated Babylon.js as well. Three.js won on bundle size (roughly 150KB gzipped for the subset we needed vs 300KB+ for Babylon) and because its BufferGeometry API gave us direct control over vertex attribute layout, which mattered for our custom shaders.
- STL processing: A Node.js microservice for binary and ASCII STL parsing, mesh validation, and LOD generation. We chose Node over a C++ service because the I/O-bound nature of streaming large files from S3 favored Node's event loop, and the CPU-bound decimation work was offloaded to a compiled WASM module.
- Backend API: .NET 8 Web API for scan metadata, authentication, and lab routing. Entity Framework Core with PostgreSQL. Nothing exotic here -- it is a CRUD API with JWT-based auth and tenant isolation.
- Cloud storage: AWS S3 with AES-256 server-side encryption, presigned URLs for time-limited access, and CloudFront for edge caching of lower-resolution LODs.
- Real-time collaboration: SignalR WebSocket connections for live annotation sharing. We considered polling but the latency requirements for collaborative review (clinician and lab tech looking at the same scan simultaneously) made WebSockets the obvious choice.
HIPAA Compliance Architecture
The key architectural constraint was that scan geometry constitutes PHI. Our approach: scan files are encrypted at rest in S3, accessed only through presigned URLs that expire after 15 minutes, and rendered entirely client-side. After the initial optimization pass in the processing service, the server never reconstructs a viewable 3D model. The browser fetches encrypted mesh data through an authenticated API gateway, decrypts it in memory, and renders it. When the session ends, the mesh data is discarded.
Audit logging captures every access event -- who viewed which scan, when, and for how long. This is stored in a separate, append-only audit database. The audit granularity was driven by compliance requirements, not by us: the compliance team wanted to know every interaction with PHI, including which LOD level was loaded.
STL Parsing and Mesh Optimization
Intraoral scanners produce STL files that range from about 15MB for a quadrant scan to over 100MB for a full-arch scan at maximum resolution. The STL format itself is simple -- either ASCII or binary, with each triangle defined by a normal vector and three vertices. But "simple format" does not mean "easy to work with at scale."
Binary STL Parsing Pipeline
The binary STL format has an 80-byte header (often garbage data from the scanner firmware), a 4-byte unsigned integer triangle count, and then 50 bytes per triangle: 12 bytes for the normal vector, 36 bytes for three vertices, and 2 bytes for an attribute byte count that is almost always zero. We built a streaming parser in Node.js that reads this structure in chunks rather than loading the entire file into memory. For a 100MB file with ~2 million triangles, peak memory usage stays under 50MB.
The parser performs validation on the fly. It checks for degenerate triangles (zero-area faces where two or more vertices are coincident), non-manifold edges (edges shared by more than two triangles), and inconsistent normals. In practice, we found that about 10-15% of scans from certain scanner models had flipped normals on a subset of faces. This would cause rendering artifacts -- some faces appearing dark or invisible because backface culling removes them. We auto-correct this using a flood-fill algorithm that propagates consistent normal orientation from a seed triangle outward.
Progressive Level-of-Detail Generation
Sending the full mesh to the browser is not viable on clinic hardware. We generate three LOD levels using Quadric Edge Collapse decimation, implemented as a compiled WASM module (ported from a C++ implementation based on the Garland-Heckbert algorithm):
- LOD 0 (Preview): Roughly 50,000 triangles. Used for thumbnail generation and the initial viewer load while the higher LODs stream in. File size is typically under 800KB after gzip compression.
- LOD 1 (Clinical review): Around 250,000 triangles. This is the level most clinicians use for routine review -- it preserves enough detail for margin line identification and occlusal surface assessment. File size is about 3-4MB.
- LOD 2 (Full resolution): The original mesh, untouched. This level is streamed only when a lab technician explicitly requests it for fabrication work.
The decimation algorithm preserves boundary edges and high-curvature regions, which is important because margin lines and cusp tips carry clinical significance. We measured geometric fidelity by computing the Hausdorff distance between the decimated mesh and the original. At LOD 1 (250K triangles from a 2M original), the maximum Hausdorff distance is typically under 0.03mm. For context, the tolerance for crown fit is around 0.1mm, so the LOD 1 mesh is well within clinical relevance.
WebGL Rendering Pipeline
The Three.js viewer needed to maintain 60fps on integrated Intel UHD 630 GPUs at LOD 1 (250K triangles). This is achievable with careful attention to draw calls, shader complexity, and texture management. Here is where we spent most of our time.
Custom Shader Development
The default Three.js materials (MeshStandardMaterial, MeshPhongMaterial) rendered dental anatomy as flat, plastic-looking surfaces. Clinicians found this unhelpful for assessing surface features. We wrote custom GLSL shaders that approximate subsurface scattering for enamel -- teeth have a characteristic translucency that affects how clinicians perceive surface detail. The shader uses a two-pass approach: the first pass computes an approximate light penetration depth map, and the second blends transmitted light with surface reflection.
For ambient occlusion, we avoided a screen-space AO pass (too expensive on integrated GPUs -- about 4ms per frame overhead on UHD 630) and instead baked per-vertex AO values during the mesh load phase. This is computed once and stored as a vertex attribute. The visual result is that fissures and interproximal areas appear darker and more defined, which helps clinicians assess preparation depth. The per-vertex approach is not as high quality as SSAO, but the performance difference was worth the trade-off.
Measurement and Annotation Tools
We built a raycasting-based measurement tool using Three.js's Raycaster class. Clinicians click two points on the mesh surface and the tool computes the geodesic distance between them along the surface, not just the Euclidean distance. This matters for curved surfaces like tooth preparations where the straight-line distance can understate the actual surface distance significantly. We implemented Dijkstra's algorithm on the mesh graph (vertices as nodes, edges as weighted connections) with edge weights equal to Euclidean edge length. It is not the most efficient geodesic algorithm, but it runs in under 100ms on LOD 1 meshes, which is fast enough for interactive use.
Annotations are stored as barycentric coordinates relative to the triangle they fall on, plus the triangle index. This means annotations survive LOD switches -- a point placed on LOD 1 maps to approximately the same surface location on LOD 2, with a positional error smaller than the mesh discretization itself. Annotation data syncs between users via SignalR, so a clinician and lab technician can review the same scan simultaneously and see each other's markups in real time.
Cloud Storage and Scan Management
Multi-location dental organizations can generate a substantial volume of scans daily. Each scan, across all LOD levels plus metadata, consumes around 25-30MB of storage. Over a year, for a moderately sized practice group, that adds up to several terabytes. The storage architecture needs to balance retrieval speed for recent scans, cost efficiency for archival data, and HIPAA compliance throughout.
S3 Storage Architecture
We organized S3 keys hierarchically: /{org_id}/{clinic_id}/{patient_id}/{scan_date}/{scan_type}/. Each scan directory contains the original STL, generated LODs, a thumbnail PNG, and a JSON metadata manifest listing file sizes, triangle counts, and processing timestamps. This key structure lets us scope IAM policies per-clinic and run S3 lifecycle rules per-organization.
We use S3 Intelligent-Tiering rather than managing lifecycle transitions manually. Scans older than 90 days that are not accessed move to infrequent access automatically. The cost savings are meaningful at scale -- infrequent access storage costs about 40% less per GB than standard. CloudFront caches LOD 0 and LOD 1 files at edge locations. Cache hit rates stabilize at around 60-70% after the first few weeks, because clinicians frequently re-open recent scans during follow-up visits.
Upload Pipeline and Processing Queue
Scans arrive from clinic workstations via an Electron tray application that watches a designated export folder for new STL files. The app uses multipart upload with automatic retry on network failures. Once the file lands in S3, an event notification triggers a Lambda function that enqueues the scan in an SQS FIFO queue. The processing service (the Node.js STL parser and LOD generator) polls this queue, processes each scan, and writes results back to S3.
Processing time depends on mesh size: a quadrant scan (roughly 500K triangles) takes about 5-8 seconds for all three LODs, while a full-arch scan (2-4M triangles) can take 15-25 seconds. The processing service auto-scales based on queue depth -- during morning clinic hours when most scanning happens, we typically need more instances than during evenings. We set the auto-scaling floor at 2 instances and the ceiling at 12, with scale-up triggered at a queue depth of 10.
Dental Lab Integration Workflow
A 3D scan viewer on its own is a nice technical demo. The actual value comes from connecting the viewer to the downstream lab workflow. Physical impressions get shipped overnight to labs, adding days before fabrication begins. Digital scans can theoretically be transmitted instantly, but only if the lab can receive and process them in a structured way.
Lab Portal and Case Management
We built a separate lab-facing portal that presents incoming scan cases in a prioritized queue. Each case packages the full-resolution STL, all clinician annotations, shade reference photos, and a structured prescription form (restoration type, material preference, special instructions). Lab technicians view the 3D scan in the same Three.js viewer with all annotations intact. This is where the barycentric annotation storage pays off -- the lab can view at LOD 2 (full resolution) and every annotation placed by the clinician at LOD 1 appears in the correct location.
Case routing is configurable: rules can assign cases to preferred labs based on restoration type, material, or turnaround requirements. The routing logic is a straightforward priority-weighted rule engine -- nothing fancy, but it eliminates the manual "which lab should this go to" decision that was previously made by front-desk staff.
Bidirectional Communication Channel
Labs frequently need clarification on margin placement or shade matching. Previously this happened over phone calls with no record. We added a case-specific messaging thread where lab technicians can place a 3D annotation on the scan, attach it to a message, and send it to the clinician. The clinician sees the annotation in spatial context when they open the case. This is more useful than describing a location in words, and it creates an audit trail of all clinical communication for the case.
The system also tracks case status (scan received, in progress, fabrication complete, shipped) and sends notifications at each transition. This is simple state-machine logic -- nothing architecturally interesting -- but it eliminated a surprising amount of phone-tag between clinics and labs.
Results and Performance Metrics
Here is what we measured after the system went into production use.
- Rendering performance: LOD 1 (250K triangles) renders at 60fps on Intel UHD 630 GPUs and above. Older integrated GPUs (HD 530 era) hit around 40-45fps, which is usable but not smooth. We recommend a minimum of UHD 620 for a good experience.
- Time to first render: LOD 0 loads in about 1-2 seconds for cached scans (CloudFront hit), 3-5 seconds for uncached. LOD 1 streams in behind it and is typically ready within 4-6 seconds total.
- Processing pipeline: Full-arch scans process through the LOD pipeline in 15-25 seconds. The SQS queue has not backed up beyond 20 items in production, even during peak hours.
- Redo rate reduction: Practices using chair-side scan validation (reviewing the scan in the viewer before the patient leaves the chair) reported fewer remakes due to scan quality issues. We did not run a controlled study, so we cannot give a precise number, but the qualitative feedback from clinicians was consistent.
- Storage costs: Intelligent-Tiering keeps costs reasonable. For a practice group generating several hundred scans per month, storage runs a few hundred dollars per month across all LOD levels.
What We Learned
The LOD pipeline was not in the original project scope. Our initial prototype sent full-resolution meshes to the viewer, which worked fine on development machines with discrete GPUs. The first time we tested on actual clinic hardware -- a 4-year-old Dell OptiPlex with integrated graphics -- the framerate dropped to single digits. The LOD pipeline was an emergency addition that became the most important component of the system. If you are building browser-based 3D visualization for any domain, budget for mesh optimization from the start.
The second lesson was that the collaborative annotation feature drove adoption more than the 3D rendering quality. Clinicians cared less about subsurface scattering shaders and more about being able to point at a spot on a scan and say "this margin needs to be adjusted." If we were starting over, we would build the communication layer first and the rendering polish second.
Healthcare Engineering
Working on Browser-Based Medical Visualization?
We have experience building HIPAA-compliant 3D rendering pipelines that run on the hardware clinics actually use. Happy to compare notes.
Get in Touch