Different Platforms, Different Fates: A Compression Selection Guide
The previous four articles covered the tools of vertex and texture compression. But knowing how to use the tools is one thing; which to use, and in what scenario is another. This article answers that—and aims to let you decide the moment you finish reading.
After this one you should be able to answer: what platform does my project run on, is the first-paint bottleneck download or VRAM, and which scheme should textures and vertices each use.
Set the tone: scenario-driven, not tool-driven
The whole article has one core principle, one the series keeps stressing:
There is no "best" compression scheme—only the "best-matching-the-scenario" scheme.
Three variables that drive the choice:
- Platform/device: desktop PC, mobile browser, VR headset, Mini Program—radically different capabilities
- Usage: one-shot display (e-commerce product page) or long immersion (VR game)
- Primary bottleneck: slow download, insufficient VRAM, or decode stalling the first paint
Think through the bottleneck first, then come back to pick tools. The matrix below freezes that thinking into place.
Core decision matrix: platform × recommended scheme
This is the most important table in the article. By platform, it gives the recommended scheme for texture and vertex, with reasons.
| Platform / scenario | Texture scheme | Vertex scheme | Main bottleneck | Key reason |
|---|---|---|---|---|
| Desktop web (PC browser) | KTX2 or WebP | MeshOpt / quantization | Download speed | VRAM is ample; focus on small files, fast load |
| Mobile web (phone browser) | KTX2 (must) | MeshOpt | VRAM | Phone VRAM is tight; textures must be block-compressed |
| WebXR / VR headset | KTX2 (must) | MeshOpt + LOD | VRAM + frame rate | VRAM blowup crashes; dropped frames cause nausea |
| WeChat Mini Program | KTX2 / WebP | MeshOpt / plain quantization | Package + compatibility | Package size is limited; avoid heavy decoders |
| E-commerce product display | WebP (light) / KTX2 (precise) | MeshOpt | First-paint speed | Demands instant open; trade size for speed |
| Large scenes / digital twins | KTX2 (must) | MeshOpt + Draco + LOD | VRAM + draw calls | Many textures, big models; compress on every front |
A few judgments worth remembering:
- Whenever VRAM is the bottleneck, KTX2 is mandatory (mobile, VR, large scenes all qualify)
- When download is the bottleneck and VRAM is ample, WebP is enough (desktop, e-commerce)
- For package-sensitive environments like Mini Programs, prefer MeshOpt over Draco—smaller decoder, better compatibility
- Only consider Draco for very large models; for the vast majority of small-to-medium models, MeshOpt is more balanced
Texture formats compared across all dimensions
Knowing "use KTX2" isn't enough. For texture formats specifically, lay out all the dimensions:
| Format | File size | VRAM usage | Upload speed | Compatibility | Quality | Use case |
|---|---|---|---|---|---|---|
| PNG | Large | Large (after decompress) | Slow | Very wide | Lossless | Needs exact values/alpha, or legacy fallback |
| JPG | Tiny | Large (after decompress) | Slow | Very wide | Lossy | Color maps, network-first |
| WebP | Very small | Large (after decompress) | Slow | Fairly wide | High | Desktop web, chasing download speed |
| AVIF | Even smaller | Large (after decompress) | Slow | Progressive | High | New platforms, extreme compression |
| KTX2 (ETC1S) | Small | Tiny | Fast | Needs transcode | Medium (color is fine) | Color maps, mobile/VR |
| KTX2 (UASTC) | Medium | Small | Fast | Needs transcode | High | Normal/data maps |
Note the first three rows (PNG/JPG/WebP/AVIF) all show VRAM as "large"—no matter how small on disk, once in VRAM they all decompress back to raw pixels. That's the fundamental limit of traditional formats.
A mixed strategy is common: key color maps use KTX2 to secure VRAM; secondary maps (e.g. a small emissive) use WebP for convenience. You don't have to go all-KTX2; allocate by bottleneck.
Decision flowchart: pick step by step
The matrix is the result; the flowchart shows how to reach it.
Where does your project run?
│
├─ Desktop web (ample VRAM)
│ └─ First paint slow?
│ ├─ Yes → WebP (or AVIF) + MeshOpt [chase download speed]
│ └─ No, but many models → KTX2 + MeshOpt [leave headroom]
│
├─ Mobile web / VR / large scenes (tight VRAM)
│ └─ Textures must use KTX2 (color ETC1S, data UASTC)
│ └─ Vertices super-dense?
│ ├─ Yes → + Draco [max ratio, can tolerate slow decode]
│ └─ No → + MeshOpt [balanced]
│
└─ Mini Program / restricted runtime
└─ Package-sensitive?
├─ Yes → plain quantization or MeshOpt + WebP [zero/small decoder]
└─ Okay → MeshOpt + KTX2 [standard combo]
Vertex vs texture compression: where to invest
A common question: budget is limited, optimize which end first? Look at the model's makeup:
| Model trait | Investment focus | Reason |
|---|---|---|
| PBR character/product (many maps) | Texture compression | Textures are 80%+; low ROI on vertices |
| CAD/scan model (super-dense vertices, few maps) | Vertex compression | Vertices are the main bulk |
| Animated character (skeleton + skinning) | Invest in both, but vertex-prioritize beyond Draco | Animation data also takes bulk; Draco is weak on animation |
| Building/scene (large, medium maps) | Texture + LOD | Textures save VRAM; LOD saves draw calls |
A rough return ranking: texture KTX2 > vertex MeshOpt/quantization > geometry simplification (LOD) > vertex Draco. Do the high-return ones first.
Mixed strategy: key maps KTX2, secondary maps WebP
Not every map is worth compressing to KTX2. A typical PBR material has 5-6 maps; compressing all to KTX2 is a lot of engineering and sometimes unnecessary.
A practical split:
- Must KTX2: albedo (large, color), normal (precision-sensitive), roughness/metallic (affect lighting)
- Can be WebP/JPG: emissive (usually small), AO (mid-low frequency), detail normal (if any)
The judgment criteria: is the map large, is it high-frequency detail, will it be sampled frequently. All three → KTX2; none → a traditional format is easier.
Automated selection: gltf-transform does it in one shot
gltf-transform's optimize command is the silver bullet for most scenarios—it auto-detects texture types, compresses textures by the recommended strategy, and optionally handles vertices.
# Install
npm install -g @gltf-transform/cli
# Standard optimization: texture KTX2 + vertex MeshOpt + de-duplication
gltf-transform optimize model.glb model-optimized.glb \
--texture-compress basisu \
--meshopt
Parameter reference:
| Parameter | Effect | Default |
|---|---|---|
--texture-compress basisu | Compress textures to KTX2 (auto ETC1S/UASTC) | Off |
--meshopt | Enable MeshOpt vertex compression | Off |
--simplify | Geometry simplification (reduces vertices, lossy) | Off |
--weld | Merge duplicate vertices | On |
--prune | Remove unused nodes/materials | On |
A "strong compression" combo suited for mobile:
gltf-transform optimize model.glb model-mobile.glb \
--texture-compress basisu \
--meshopt \
--simplify --simplify-ratio 0.5 \
--prune --weld
A "gentle" desktop combo (preserve detail, only compress textures and vertices):
gltf-transform optimize model.glb model-desktop.glb \
--texture-compress webp \
--meshopt
Wrap it as a reusable script
Encapsulate the above into a script that outputs a version per platform:
// compress.mjs — output different compressed versions by target
import { optimize } from "@gltf-transform/functions";
import { NodeIO } from "@gltf-transform/core";
import { KHRONOS_EXTENSIONS } from "@gltf-transform/extensions";
const io = new NodeIO().registerExtensions(KHRONOS_EXTENSIONS);
const targets = {
// Mobile: KTX2 + MeshOpt + simplify
mobile: { texture: "basisu", meshopt: true, simplify: 0.5 },
// VR: KTX2 + MeshOpt, no simplify (close-up viewing in VR)
vr: { texture: "basisu", meshopt: true, simplify: null },
// Desktop: WebP + MeshOpt, gentle
desktop: { texture: "webp", meshopt: true, simplify: null },
};
const input = process.argv[2];
const doc = await io.read(input);
for (const [name, cfg] of Object.entries(targets)) {
const out = doc.clone(); // independent copy per target
await optimize(out, {
textureCompression: cfg.texture,
meshCompression: cfg.meshopt ? "meshopt" : null,
simplify: cfg.simplify != null ? { ratio: cfg.simplify } : null,
});
await io.write(`model-${name}.glb`, out);
console.log(`✓ model-${name}.glb`);
}
node compress.mjs model.glb
# Outputs model-mobile.glb / model-vr.glb / model-desktop.glb
At runtime, load the matching version by device (distinguish via UA or WebGL capability probing) for the best first-paint experience. The next article's end-to-end walkthrough strings this all together.
One-line cheat sheet
- VRAM is the bottleneck → KTX2, no choice
- Download is the bottleneck, VRAM ample → WebP/AVIF also fine
- Sensitive to decoder size → MeshOpt > plain quantization > Draco
- Very large models → only then consider Draco
- Unsure → run
gltf-transform optimizein one shot; adjust if wrong
What's next
The selection framework is in place. The final article wraps up: starting from a real model, walk the full flow of Blender export, texture compression, vertex compression, and engine loading, with a complete automation script and a series cheat sheet.