ldr
v0.33.14
Published
High-performance LEGO (LDraw) rendering for three.js: instanced parts packs, hidden stud/tube culling, low-detail studs, indexed edges, build steps.
Maintainers
Readme
ldr
Render 100,000-brick LEGO® (LDraw) models in three.js at interactive frame rates.
ldr compiles the parts a model uses into a compact binary parts pack ahead of time. At runtime it draws them with GPU instancing, hidden-stud and underside culling, distance LOD (simplified meshes, sub-pixel studs dropped, per-cluster frustum culling), single-sided (BFC) faces and indexed edge lines. On an Apple M4 Pro that takes a fleet of 75 UCS Millennium Falcons (394,950 bricks) from 205 ms to 29 ms per frame, and its load time from 2.4 s to 1.0 s.
· MIT · ESM + TypeScript ·
three is a peer dependency
- Spec-compliant LDraw reader:
.ldr,.mpdand.dat, including MPD-embedded parts, geometry drawn directly in models, BFC,STEP/ROTSTEP,!TEXMAPand!DATA. createLdrObject()returns aTHREE.Groupyou can drop into any scene, plus a build-steps API.ldr packCLI andbuildPack()build parts packs from your LDraw library, compressed with zstd or gzip for serving.- Tree-shakeable (
sideEffects: false). The browser entry isldr; the filesystem tools are inldr/node.
Install
bun add ldr three # or: npm i ldr threeYou also need the LDraw parts library, which is not bundled, to build packs. Download complete.zip from https://library.ldraw.org and unzip it to an ldraw/ folder containing LDConfig.ldr, parts/ and p/. Apps only need the pack at runtime, not the library.
Quickstart
1. Build a parts pack (CLI, once at build time)
bunx ldr pack --ldraw ./ldraw -o public/car.pack.zst models/car.ldrldr pack --ldraw <dir> -o <out.bin|.zst|.gz> [model.ldr ...] [--part 3001.dat ...] [--all-parts]
[--studs low|full] [--prims low|full] [--no-weld] [--no-colors]
[--cond] [--textures] [--no-loose-geometry] [--no-lod] [--reorder]The pack holds only the parts your models reference, plus the LDConfig colour table. A .zst or .gz extension compresses the output.
2. …or build it from code (Node / Bun)
import { buildPack } from "ldr/node";
const { data, parts, missing } = buildPack({
ldrawRoot: "./ldraw",
models: ["models/car.ldr"], // paths, or { name, text }
studs: "low", // default
cond: false, // conditional lines (opt-in)
textures: false, // printed/sticker parts (opt-in)
});
await Bun.write("public/car.pack.bin", data); // raw; compress with zstd/gzip for serving
console.log(`${parts} parts`, missing.length ? `missing: ${missing}` : "");3. Render it (browser)
import * as THREE from "three";
import { parseModel, loadPack, createLdrObject } from "ldr";
const [text, pack] = await Promise.all([
fetch("/car.ldr").then((r) => r.text()),
loadPack("/car.pack.zst"), // .zst / .gz / raw .bin (detected from the bytes)
]);
const model = createLdrObject(parseModel(text, "car.ldr"), pack, {
edges: true, cullStuds: true, cullUndersides: true, side: "front", material: "plastic",
});
scene.add(model.object); // +Y up, 1 unit = 1 LDU (see `scale`)
// Build steps
model.setStep(0); // show step 0 … model.stepCount - 1
model.setStep(null); // show everything
const view = model.stepViews[3]; // ROTSTEP view for step 3 (or null)Light the scene (or set scene.environment) as you would for any MeshStandardMaterial or MeshPhysicalMaterial.
API reference
ldr (browser-safe)
Parsing and model
| export | description |
|---|---|
| parse(text, name?, { geometryText? }) → Doc | Parses LDraw text into files and commands. MPD-aware: 0 FILE/0 NOFILE and 0 !DATA blocks. |
| flatten(doc, options?) → PartInstance[] | Expands submodels into placed parts. Each instance has part, color, a 12-number matrix m, step, nested steps, path and the ROTSTEP view of its step. colour (an LDrawColor) is set when a 0 !COLOUR line in the model itself defines the instance's colour. colPart / colSource name the part with the model's definitions in scope applied inside it (a synthetic part, <part>#col-<hash>; packs from buildPack have it only when it changes a colour, and the renderer falls back to part). |
| parseModel(text, name?, options?) | parse followed by flatten. |
| sequenceSteps(instances) → { count, stepOf, views } | Gives each instance a global build-step index, sequencing nested submodel steps. views holds the ROTSTEP view per global step. |
| parseLDConfig(text) → Map<code, LDrawColor> | Reads a !COLOUR table into { code, name, hex, edge, alpha, luminance, finish } (CHROME / PEARLESCENT / RUBBER / METAL / MATTE_METALLIC / MATERIAL GLITTER|SPECKLE|FABRIC). |
| blendedColourHex(code) | RGB of a blended (old dithered) code 256-511, for codes LDConfig doesn't define. |
| parseColourLine, colourSurface, colourMaterialKey, makeFinishMaterial | Spec finish → PBR surface (metalness/roughness/clearcoat/sheen; glitter/speckle use cheap procedural grain). |
| fileKind(doc, name) | Decides whether an MPD file is a part or a model: by its header type, otherwise by whether it has its own geometry. |
| looseSteps / looseFile / looseSource / loosePartName | A model's own type 2–5 geometry, per step, as synthetic parts named <file>#<step>. |
| rotStepMatrix / stepViewMatrix / fileStepViews | ROTSTEP views, following MLCad: REL, ADD, ABS, END. A ROTSTEP ends its step. |
| parseTexMap / texmapUV / texmapTriangleUV / transformTexMap / resolveTexture / texWrapper | !TEXMAP support (PLANAR, CYLINDRICAL, SPHERICAL). UV conventions follow LDView: v = 0 is the image's top row. |
| parseColor, isDirectColor, namedColorCode, isNamedColor, colorToken, mul, invert, IDENTITY, norm, smoothNormals, hash32 | Low-level helpers. |
FlattenOptions:
| option | default | description |
|---|---|---|
| looseGeometry | true | Emits a model file's own type 2–5 lines as synthetic parts <file>#<step>. LDraw allows geometry in any file. |
| textures | false | TEXMAP-aware view of the model. References wrapped by a model-level !TEXMAP get a texPart, and !TEXMAP FALLBACK references are skipped. Use with textures: true packs. |
| resolveModel(name) | – | Resolves an external reference to a Doc, such as a model loaded separately. Return null to treat it as a part. |
| maxDepth | 64 | Submodel nesting limit (cycle guard). |
MPD-embedded parts, such as Studio/OMR unofficial parts or stickers, become part instances (embedded: true) and override library parts of the same name. loadModel asks the parts CDN for them by a hash of their definition (embeddedKeys), so a CDN built with --embedded <model dir> serves the embedded parts of those models (the demo CDN carries the ones in its 1,835 catalog models).
Packs and rendering
| export | description |
|---|---|
| loadPack(url) → Promise<Pack> | Fetches, decompresses and decodes a pack. zstd and gzip are detected automatically. |
| decompressPack(bytes) / decodePack(buffer) | Same, from bytes you already have. |
| decodeModelText(bytes) → Promise<string> / modelFileName(url) | Model file bytes to LDraw text (zstd / gzip detected and inflated, then UTF-8), and the file name loadModel uses for a URL (8466-1.mpd.zst → 8466-1.mpd). loadModel reads model URLs this way, so large models can be hosted as .mpd.zst. |
| bodyTriangles(insts, pack) / BudgetError / maxBodyTris / maxPackBytes | Full-detail body triangle count; prepareModel / loadModel abort with BudgetError when the merged pack exceeds the caller's triangle or byte limits (phones). |
| lowCdn / tier | Lower-detail tier of the parts CDN (the demo's is <cdn>low/, built by build-parts-cdn.ts --low: per-part simplified bodies, kept only where they are no more see-through than full detail and still look like the part: at most 30% of pixels off in flat-shaded views). With a budget set, a model over it at full detail is checked against and loaded from lowCdn; BudgetError only when it is over there too. LoadedModel.tier / PreparedModel.tier is "full" or "low". |
| promoteFlexParts / decimateFlexGeometry / prepareFlexParts (node) | Relabel LDCad flex Model files as embedded parts, then lower tube ring tessellation (16→8) and drop rib-detail quads for cheaper hose meshes. |
| createLdrObject(instances, pack, options?) → LdrObject | High-level three.js object (see below). |
| createInstancedEdges(pos, instancedMesh, material?) | Welded, indexed edge lines that share an InstancedMesh's instance matrices. |
| conditionalLines(pack, entry, which?) / createInstancedConditionalEdges(cond, instancedMesh, material?) / conditionalLineVisible | Type-5 conditional lines from a cond pack. A vertex shader keeps a segment only where both control points project to the same side of the line. |
| makeTexturedMaterial(side, texture, wrapU?, base?, gloss?) / packTexture(pack, i, data?) / pngHasAlpha(bytes) | Printed and sticker parts from a textures pack: a UV material and the decoded embedded PNG. gloss ({ map, alpha }, a GLOSSMAP PNG decoded with data = true) adds specularity where its alpha is set (its grey value if the PNG has no alpha channel). |
| buildLdrScene, computeCulling, cullCore, makeMaterial | Low-level renderer used by the benchmarks. cullCore(instances, header, steps?) is the typed-array culling core (with steps, also the step at which each stud and underside becomes hidden). |
LdrObjectOptions:
| option | default | description |
|---|---|---|
| edges | true | LDraw type-2 edge lines, indexed and sharing each part's instance matrices. |
| cullStuds | true | Hides studs fully covered by a part above. |
| cullUndersides | true | Hides undersides (tubes) sitting fully on studs below. |
| side | "front" | "front" draws single-sided BFC faces; "double" draws both sides. Uncertified parts are always double-sided. |
| material | "plastic" | "plastic" is MeshPhysicalMaterial with clearcoat; "standard" is MeshStandardMaterial, the cheapest option. |
| conditionalLines | true | Conditional (silhouette) lines. Only takes effect with cond packs, and follows edges. |
| conditionalStudLines | true | Conditional lines on studs. false roughly halves the conditional-line cost on large scenes. |
| textures | true | Renders !TEXMAP images from textures packs, with GLOSSMAP images as extra gloss (lower roughness, more specular) on the printed area; false draws the FALLBACK geometry. object.texturesReady resolves once the images are decoded. |
| colors | pack table | Colour override: a Map, a record, or (code) => color. |
| fallbackColor | #A0A5A9 | Colour used for unknown codes. |
| ldrawSpace | false | true keeps native LDraw space (-Y up). |
| scale | 1 | Uniform scale (1 = 1 LDU; 1 stud = 20 LDU). |
| step | all | Initial build step (inclusive, 0-based). |
| lod | true | Distance LOD, re-selected for each camera you render with. Batches are split into spatial clusters (off-screen ones are frustum-culled). Each cluster draws the coarsest simplified mesh (from lod packs) whose error stays under lodPx, swaps closed studs to a 10-triangle square stud and open (hollow) studs to a 24-triangle square stud with a square hole (each with the square outline of its top), then a flat disc, and drops them once their 12-LDU disc is under lodPx, and hides edges where a stud spans less than lodEdgePx. |
| lodPx | 0.5 | LOD: maximum screen-space error in pixels. |
| lodEdgePx | 0.5 | LOD: draw edge and conditional lines only where one stud (20 LDU) spans at least this many pixels. 0 = always. |
| lodEdgeMinPx | 0.4 | LOD: draw only edge segments at least this many pixels long on screen. 0 = every segment. |
| lodEdgeCap | 0.75 | LOD: a part draws at most lodEdgeCap × d² of its edge segments (its longest), d = its size on screen in pixels. 0 = no cap. |
| lodUnder | true | LOD: skip undersides that can't be seen (camera behind every bottom face of a cluster's parts, or the body drawn as its closed bounding box). Lossless in the fidelity checks. |
LdrObject: object, stats (instances, drawn, missing[], studsHidden/Total, undersHidden/Total, buildMs, condBatches, texBatches, textures, glossmaps, clusters), stepCount, stepViews, texturesReady, setStep(n | null), setEdges(on), setConditionalLines(on), setCulling(studs, undersides), setSide(side), setLod(on), setGhost(on, { opacity, color, whiten }?), getBox(target?), dispose().
setStep doesn't rebuild anything: it compacts instance buffers, and culling is step-aware (a stud covered only by a later step's part stays visible). stats reflect the shown step.
setGhost(true) draws the steps still to come as a faint, whitened, depth-written ghost (like the LEGO Builder app). It is built once on first use, without edges, with the steps reversed so setStep stays a buffer compaction for it too. It honours the booklet isolate mask, and getBox ignores it. Off by default. Cost: about the same as drawing the whole model (Titanic, 9.7k bricks: 1.6 ms off, 2.5 ms on, 2.8 ms full model).
Building instructions. parseModel(text, name, { callouts: true }) (or loadModel(url, { callouts: true })) sequences a
submodel placed N times in one step like a booklet: the first copy is built step by step (its instances get mult = N, the "xN"),
the other copies appear finished at the end of that step. planStepViews(instances, pack.header.parts, stepOf, stepCount) plans a
camera per step (view(step) → { dir, target, radius, minVis, meanVis, below, isolate }, LDraw space): the booklet view (30° up,
45° round) while it shows the new parts at least half visible on average, otherwise the best of 24 views from above (or from below for
parts added underneath), preferring front three-quarter views and small turns (thresholds from Agrawala et al., SIGGRAPH 2003).
isolate is set while a sub-assembly is being built: show it on its own with setStep(step, isolateMask(instances, view.isolate)).
Visibility is measured on part bounding boxes, so it needs no GPU and also runs in Node.
ldr/node (Node / Bun only)
Everything above, plus:
| export | description |
|---|---|
| buildPack(options) → { data, parts, studKinds, missing, warnings? } | Builds a pack. Options: ldrawRoot, models, parts, allParts, studs ("low" | "full"), primitives ("low" | "full", default "full"; "low" substitutes p/8 primitives and leaves gaps in round parts), weld (true), cond (false), textures (false), looseGeometry (true), colors (true), lod (true: meshoptimizer-simplified LOD meshes, pack feature lod), reorder (false: vertex-cache order), onProgress. |
| Library(root, unofficialRoot?) | Filesystem LDraw resolver: where, flatten, mesh ({ texmap: true } adds uv, per-triangle texture, textures and, when a map has one, glossmaps), colors, texture, register. Handles case-insensitive lookup and ~Moved to aliases. |
| Compiler, encodePack, writePack, loadColors | Lower-level pack compiler. |
| ResolverLibrary, splitModel, flattenWithLibrary, collectDeps | The resolver that buildPack uses. |
Benchmarks
All numbers below were measured with the harness in bench/ and are taken from the result files there.
- Hardware and browser: Apple M4 Pro MacBook Pro, Chrome with native GL (ANGLE → Metal), Bun 1.3.
- Frame time: the median synced frame time (
gl.finishper frame) while orbiting the camera. - Load: from fetch to the first rendered frame.
- Baseline: three.js geometry from
LDrawLoadermerged per part and drawn withInstancedMesh, which is the fastest stock approach. StockLDrawLoaderscenes crash the tab (OOM) at 50k bricks and above. - ldr settings: the defaults: LOD pack, distance LOD, low-detail studs, stud and underside culling, single-sided faces, no edges.
Sources: bench/results/2026-10-18T18-10-46-518Z-falcon-vs-stock.md (the Falcon rows), bench/results/latest-perf.md (the regression gate, bench/baselines/darwin-apple-m4-pro.json), latest-mac-ablation.md, and research.md §7 and Round 19.
| model | bricks | baseline frame | ldr frame | baseline load | ldr load | |---|---|---|---|---|---| | synthetic grid | 100,021 | 24.4 ms | 2.7 ms | 0.2 s | 0.3 s | | Millennium Falcon ×2 | 10,532 | 8.0 ms | 8.2 ms | 1.98 s | 0.14 s | | Millennium Falcon ×19 | 100,054 | 54.2 ms | 19.6 ms | 2.04 s | 0.38 s | | Millennium Falcon ×75 | 394,950 | 204.7 ms | 29.2 ms | 2.40 s | 1.03 s |
The benchmark model is a fleet: the LDraw OMR model of set 10179 (UCS Millennium Falcon, 5,266 parts, 251 unique) placed 2, 19 or 75 times on a grid (apps/demo-site/scripts/build-fleet.ts). It is a hard case for ldr: dense, detailed ships stay large on screen, so distance LOD can't drop much and the overview still draws ~19M triangles (32M before Round 62's cluster-box distances, 27M before Round 63's square-stud mid LOD, 22M before Round 66's 40% level for part bodies); on two ships ldr only matches stock instancing per frame (it still loads 14× faster). Datsville, the benchmark until Round 60 (a flat town, 392,514 bricks, mostly baseplates), went from 409.6 to 12.5 ms per frame and 24.7 to 1.6 s load.
How much each optimization contributed on Datsville 100k (Rounds 1–20, before the Falcon fleets), in ms per frame: welding 108.1 → 64.6, stud culling → 50.8, underside culling → 41.7, low-detail studs → 31.0, single-sided faces → 30.1. Triangles per frame drop from 142.7M to 59.1M. Distance LOD (Rounds 19–20: simplified and bounding-box levels with a silhouette-coverage error, sub-pixel studs dropped, undersides skipped where they can't be seen) then takes it to 4.4 ms and 4.0M triangles; ≤ 1.5% of pixels differ from full detail (mostly sub-pixel stud moiré).
- Pack size: the Falcon fleet (251 unique parts) is 26.6 MB raw and 1.85 MB zstd with LOD meshes, at any number of ships (24.5 / 1.65 MB before Round 66 added a 40% LOD level to part bodies). Decoding a zstd pack in the browser (fzstd) takes about 60 ms.
- Stock
LDrawLoader: loading the scene directly crashes the renderer (OOM) on the 50k, 100k and 250k grids (and on Datsville 10k). The 10k grid loads in 6.6 s and draws at 61.5 ms per frame (29,671 draw calls), against 0.15 s and 2.2 ms with ldr + edges. - Original demo app (retired; rAF frame median at 2880×1800 on the M4 Pro):
- These comparisons are between demo settings. "Before" has every effect on (shadows, AO, screen-space edges, PBR/IBL); "after" is the demo's big-model defaults, which turn those effects off above 50k bricks.
- Full Datsville: 1.7 → 9.9 fps.
- Datsville 100k: 8 → 43.5 fps.
- Conditional lines (opt-in
condpacks) cost +12–14 % frame time on Datsville 10k and 100k (M4 Pro, before Round 60), and add 7–14 % to the zstd pack size. - Edges: with LOD, edges take the Falcon fleet from 9.8 → 12.9 ms (19 ships) and 18.3 → 22.9 ms (75 ships) on the Mac Mini M6 (Round 67); before Round 62 they cost 12.8 → 33.3 and 19.3 → 45.1 ms there. A simplified level draws its own outline: square studs a square per stud loop (8 segments instead of 16-32), bodies at their bounding-box level the box's 12 edges, and a line batch whose edge LOD leaves no segment issues no draw. Each cluster draws only edge segments at least 0.4 px long on screen (
lodEdgeMinPx; 0.15 px until Round 62, when 85% of the segments drawn at the fleet overview were 0.15–0.5 px long and each darkened a whole pixel, so far ships looked charcoal) and none where bricks are sub-pixel (lodEdgePx). A part also draws at most 0.75 × d² of its segments, its longest, d = its size in pixels (lodEdgeCap, Round 72), so a 6 px technic beam no longer draws ~100 segments into ~20 pixels. Far views drawn with edges get lighter: 2.6% of pixels differ from every-edge rendering on the 19-ship overview, 0.8% on the SF city, up to 4.7% on Datsville overviews.lodEdgeMinPx: 0, lodEdgePx: 0, lodEdgeCap: 0keeps every edge. - Build steps:
setSteptakes 39 ms on the 19-ship fleet (26,619 steps); on Datsville 100k it went from a full rebuild (752 ms) to 66 ms. - Visual fidelity: pixel diffs show culling and single-sided faces are lossless (≤ 0.009 % of pixels differ). Low-detail studs only change stud silhouettes.
Reproduce: cd bench && bun run setup && bun run perf (regression gate) or bun run bench (see bench/README.md).
LDraw spec compliance
- Line types 0–5. BFC (ldraw.org article 415):
CERTIFY/NOCERTIFY(decided by the first BFC statement before the file's first type 1-5 line, so0 BFC CCWalone certifies and a lateCERTIFYdoesn't),CCW/CW,CLIP/NOCLIP(polygons drawn while culling is off are visible from both sides, including in subfiles referenced then, which can't turn it back on),INVERTNEXT(the immediately following subfile reference only), inverted matrices, and degenerate (determinant 0) matrices: a subfile that lies flat at y = 0 is repaired the way LDView and L3P do (an all-zero row, or else the all-zero Y column, gets a 1, and the repaired determinant decides the winding); any other singular reference, including a part placed squashed flat in a model, is drawn without culling. - MPD:
0 FILE/0 NOFILE, and0 !DATA(base64 PNGs). An MPD file is a part or a model by its!LDRAW_ORGtype, otherwise by whether it has its own geometry. Embedded parts override library parts. - Geometry drawn directly in model files (type 2–5 lines outside parts), on by default.
0 STEP, nested submodel step sequencing,0 ROTSTEP(REL/ADD/ABS/END), and0 BUFEXCHG.!TEXMAPSTART/NEXT/FALLBACK/END, withPLANAR,CYLINDRICALandSPHERICALprojections (orientation matches LDView),GLOSSMAP(alpha = gloss amount, same UVs), and model-level!TEXMAParound part references.- Colours: LDConfig
!COLOUR, edge colours, main/edge colour inheritance (16/24), and direct colours (0x2RRGGBB). !COLOURlines in a model follow the spec's scope rule. A definition applies from its line to the end of its file and to the files that file references, and it never leaks back to the parent. It sets the instance colour and finish.!COLOURlines inside a part (an MPD-embedded part and its subparts) follow the same rule when the part is compiled, and a model file's own loose geometry sees that file's definitions: its triangles and studs take the part's own definitions (RGB; baked part colours are opaque, as for LDConfig codes). A model's definitions also reach the colours hard-coded inside the parts it places ("passed to subfiles"):buildPackcompiles a variant of such a part with those definitions applied (only where one changes a colour the part uses), and the renderer draws it; the part's own later definitions still win inside it. Parts from the hosted parts CDN (loadModel/prepareModel) don't have these variants yet, so there the colours inside parts come from LDConfig or the part's own definitions.- Named colour codes: a
!COLOURCODE may be an identifier instead of a number (0 !COLOUR My_Red CODE MyRed VALUE #FF0000 EDGE #333333, then1 myred ...), which the spec allows outside strict LDraw compatibility. Identifiers are case-insensitive and follow the same scope rule.parseColorturns one into a stable negative code (namedColorCode,isNamedColor), andcolorTokenturns it back into the identifier. A named code with no definition in scope draws the fallback colour. - Blended colour codes 256-511 that LDConfig doesn't define (used by old models, e.g. 382 in Datsville) render as the spec's average of two ldraw.exe colours instead of the fallback grey.
- Case-insensitive, backslash-normalized file lookup;
~Moved toalias parts.
Limitations
- Finishes:
!COLOURCHROME / PEARLESCENT / RUBBER / METAL / MATTE_METALLIC map to MeshStandard/Physical PBR (same numbers as three.jsLDrawLoader). MATERIAL GLITTER / SPECKLE use a procedural grain approximation; FABRIC uses sheen. ALPHA sets opacity; LUMINANCE drives emissive. - Transparency: transparent colours (
ALPHA < 255) render with their opacity anddepthWrite: false(one material bucket per finish × opacity class), fromldr packpacks and, since 0.31, from the parts CDN (loadModel), whose index now carries the colour materials. Studs and undersides behind a transparent part are never culled. Transparent batches are sorted per batch (InstancedMesh), not per instance or triangle, so overlapping transparent parts can blend in the wrong order. - Opt-in pack features: conditional lines need packs built with
cond. Printed parts show their images only fromtexturespacks (the embedded PNGs dominate the pack size); default packs draw the FALLBACK geometry. - ROTSTEP is reported (
stepViews,instance.view), not applied;planStepViewstakes afixed(step)override if you want to honour it. - Packs are static: they hold only the parts they were built for. A model that uses parts missing from the pack lists them in
stats.missing. - Occlusion culling: beyond hidden studs and undersides there is no occlusion culling (bricks enclosed by other bricks are still drawn). Frustum culling and LOD apply per spatial cluster.
- LOD and edges: edge LOD drops sub-pixel edge segments and hides edges where bricks are a fraction of a pixel, which visibly lightens far-away views drawn with edges (2.6% of pixels on the 19-ship Falcon overview, up to 4.7% on Datsville overviews). Set
lodEdgeMinPx: 0, lodEdgePx: 0, lodEdgeCap: 0to keep every edge (much slower on large models), orlod: falseto turn LOD off entirely.
Attribution and license
- Code: MIT © Evan Bacon.
- LDraw parts: packs you build contain geometry from the LDraw.org Parts Library, which is licensed CC BY 2.0 (newer files CC BY 4.0, per file header). If you ship packs, credit "LDraw.org Parts Library (CC BY 2.0)" in your app. See https://www.ldraw.org/legal-info.
- Millennium Falcon: the benchmark fleets place the LDraw OMR model of set 10179 by Roland Dahl [RolandD] (CC BY 2.0) N times.
- Trademarks: LEGO® is a trademark of the LEGO Group, which does not sponsor, authorize or endorse this project; ldr is not affiliated with the LEGO Group. LDraw™ is a trademark of the Estate of James Jessiman.
