How to Optimize 3D Assets for Unity
Configure Unity model import, mesh and vertex compression, LOD, textures, batching, memory, and profiling against real target builds.
Unity optimization starts with the imported asset Unity actually builds, not the triangle count shown in the source DCC. Import settings can change scale, topology, normals, tangents, precision, animation data, CPU copies, and platform texture formats. The render pipeline then adds batches, shader passes, shadows, lightmaps, and render targets.
Create one measured target profile per device tier and render pipeline. A URP mobile build, HDRP desktop configurator, and WebGL build need different limits.
Profile a player build#
Editor timing includes Scene view, Inspector, asset database, development checks, and editor-only allocation. Build the target player with representative quality settings and connect the Unity Profiler. Use the Frame Debugger for individual draw events and the Memory Profiler for retained resources.
Record frame time in milliseconds, batches, SetPass calls, triangles, vertices, visible skinned meshes, texture memory, mesh memory, render-target memory, and upload activity. At 60 Hz the entire frame has 16.67 milliseconds; at 30 Hz it has 33.33 milliseconds.
Capture a fixed worst-normal scene on the lowest supported device for at least 10 minutes to reveal thermal throttling. One fast editor frame is not an acceptance test.
Lock scale and coordinate intent#
Unity conventionally treats 1 unit as 1 meter. Verify a known dimension after import. Scale Factor and unit conversion can make a character 100 times too large or small while looking correct after compensating transforms.
Apply a stable axis conversion in the export/import path. Avoid nested correction transforms that complicate animation, physics, and batching. Check negative scale because it can reverse winding and tangent handedness.
Choose pivots for interaction and animation before LOD creation. Every level must share origin, orientation, and bounds closely enough to avoid visible jumps.
Control imported mesh data#
Inspect final Mesh attributes. Remove channels only when the runtime does not use them:
- tangents are needed for tangent-space normal mapping;
- UV1 or later sets may support lightmaps or custom shaders;
- colors may contain masks;
- joints and weights drive skinning;
- blend shapes can dominate character memory;
- normals may be imported, calculated, or omitted for unlit use.
Disable Read/Write Enabled when scripts, CPU deformation, runtime mesh access, or certain systems do not require it. Unity documentation explains that disabling it allows CPU-addressable mesh data to be released after GPU upload. Keeping it can effectively retain an additional mesh representation.
Test systems such as MeshCollider cooking, dynamic modification, and specialized batching before enforcing the flag globally.
Optimize Mesh or modern equivalent ordering settings can improve vertex and index locality. Measure the imported result and do not reorder after a bake if a tool depends on stable indices.
Distinguish Unity's compression choices#
Unity documents two different mesh compression paths. Mesh Compression reduces stored build or AssetBundle size, then decompresses at load, adding CPU time, temporary memory, and possible artifacts. Vertex Compression changes eligible runtime attribute precision, reducing in-memory data and bandwidth.
These are not interchangeable. Current documentation notes eligibility constraints for vertex compression, including Read/Write disabled, non-skinned meshes, platform FP16 support, Mesh Compression off, and batching conditions.
Build candidates at Off, Low, Medium, and High mesh compression. Compare:
- player or bundle bytes;
- load duration;
- peak temporary memory;
- vertex positions and silhouette;
- normals under moving highlights;
- UV alignment and fine text;
- animation where applicable;
- post-load mesh memory.
For vertex compression, inspect which channels actually use reduced formats in the player. If an eligibility rule forces FP32, the checkbox expectation is not evidence.
Simplify before engine compression#
Mesh Compression encodes the topology it receives. It does not reduce triangles or skinning work. Generate LOD geometry offline or through a validated tool, protect silhouette and deformation, then let Unity encode the approved result.
Track rendered vertices as well as triangles. Hard normals, UV seams, materials, and skin data split vertices. A source with 50000 positions can import far more vertex rows.
For skinned characters, test extreme poses. A 70% triangle reduction that passes in bind pose can collapse shoulders or eyelids. Review bone influences and blend shapes separately.
Keep collision meshes simple and independent. Do not use a 120000-triangle visual mesh as a MeshCollider when primitive or authored convex proxies satisfy gameplay.
Build an LODGroup from screen evidence#
Unity LODGroup thresholds use relative screen height. Create LOD0, LOD1, and later levels with consistent pivots, renderers, materials, and bounds. Imported naming conventions can automate setup, but verify the resulting group.
Find the largest screen size where each lower level passes. Add hysteresis-like transition policy or cross-fade where the render pipeline and shader support it. Cross-fade renders overlap and is not free.
Example candidates might contain 90000, 35000, 12000, and 3000 triangles, but ratios are only search points. Set transitions from contour and animation comparisons, not the counts.
Recalculate bounds carefully. One outlier bone or vertex can keep a high LOD active too long. Test different camera fields of view and scaled instances.
Optimize materials and batches#
Unity's Rendering Profiler reports Batches and SetPass Calls. A shader can contain multiple passes, so one Renderer does not imply one draw. Shadows, depth, lighting, and renderer features multiply work.
Share material assets when values and keywords are identical. Accessing Renderer.material can instantiate a material; use sharedMaterial when mutation is not intended, or MaterialPropertyBlock where compatible with the batching path.
Static batching, dynamic batching, SRP Batcher, and GPU instancing solve different cases and have version/pipeline constraints. Confirm with the Frame Debugger why an object did or did not batch.
Merge static geometry only within useful culling cells. One enormous bound saves submissions but draws hidden triangles. Instance repeated props when geometry and material are compatible.
Limit shader keyword variants. A small unique material can create compilation, build, state-change, and memory cost beyond its triangle region.
Texture import is platform-specific#
Source PNG or TIFF bytes do not describe runtime memory. Unity imports textures per target platform into formats such as ASTC, ETC, or BC depending on settings and support.
Set Max Size from projected use. Reducing 4096 to 2048 removes 75% of texels. Configure type correctly: normal maps need normal handling; masks and data channels need linear sampling; base color usually uses sRGB.
Review compression in a target build. ASTC block size trades quality against bits per pixel. Fine UI-like text, normal maps, alpha masks, and broad gradients fail differently.
Enable mipmaps for ordinary 3D sampling and test padding/streaming. Texture Streaming can reduce resident high mips, but requires budgets and useful priority. UI or exact data textures may need different policy.
Channel-pack compatible masks only when they share resolution, UVs, sampling, and lifecycle. Packing a low-detail metallic mask with a high-detail channel can waste memory.
Animation and skinning#
Animation clips can retain dense keys. Import only required clips, remove constant or redundant curves within tested tolerances, and choose compression against contact and facial motion.
Limit bone weights according to quality and platform. Fewer influences can reduce work but alter deformation. Test joint extremes, not only normal playback.
Cull or reduce off-screen animation where gameplay permits. Animator, rig, and script evaluation can remain CPU-heavy even when a SkinnedMeshRenderer is not visually important.
Blend shapes add per-vertex delta data. Strip unused shapes per build or asset variant. A face with 52 shapes can use far more memory than its base triangle count suggests.
Use the CPU and GPU profilers to determine whether skinning is the actual limit before reducing character quality.
Shadows and render-pipeline costs#
Every shadow-casting renderer can enter additional passes. Reduce shadow distance, cascade count, map size, and caster set according to the scene. Small distant props may not need shadows; a lower-detail shadow mesh can suffice.
Transparent surfaces add sorting and overdraw. Prefer opaque or alpha-clipped representations when appearance permits. Double-sided shaders increase work and can hide bad winding.
URP and HDRP have different features, pass structure, and shader costs. Document the exact renderer asset and quality tier with performance results.
Render scale, MSAA, post-processing, additional lights, and camera stacking can dominate before mesh geometry. Use controlled tests rather than decimating every model.
Addressables and loading#
Organize assets so initial scenes do not pull every product, LOD, or variant into memory. Addressables or AssetBundles can stage content, but dependencies may retain shared textures and materials.
Measure download, dependency resolution, decompression, deserialization, upload, and first-use shader preparation. A smaller compressed bundle can require more temporary memory and CPU.
Release handles when ownership ends, but do not release shared dependencies still in use. Cycle the heaviest scene transition 10 times and compare memory snapshots.
Keep immutable content versions and CDN caching. A rollback should repoint to an earlier built artifact, not attempt to reverse lossy import processing.
Use the Frame Debugger and Profiler together#
The Frame Debugger explains what rendered and why a batch split. The CPU Profiler explains script, animation, culling, and render-thread time. The GPU Profiler shows passes and shaders where supported. Memory Profiler snapshots show what remains and why.
Run isolation controls:
- replace materials with one cheap shader;
- force a low LOD;
- halve render scale;
- disable shadows;
- freeze animation;
- hide half the Renderers;
- replace textures with tiny placeholders.
If halving each render dimension produces a large gain, investigate fragments. If low LOD helps, investigate geometry and skinning. If one material helps, investigate passes, keywords, and batching.
Automate import gates#
Use AssetPostprocessor or build validation to report and enforce profile limits:
- dimensions and scale;
- triangles and vertices by LOD;
- submeshes and materials;
- Read/Write state;
- attribute channels;
- blend shapes and bones;
- texture dimensions and target formats;
- mip and streaming settings;
- collider complexity;
- asset labels and bundle ownership.
Allow reviewed exceptions tied to measured scene headroom. A hard numeric gate without visual context can remove important features; no gate makes budgets aspirational.
Keep platform overrides reproducible#
The same source asset can produce different Android, iOS, desktop, console, and WebGL data. Store platform overrides in version control or an auditable import preset rather than adjusting Inspectors manually on one workstation.
For every tier, export a build report containing the resolved texture formats, maximum dimensions, mesh compression, stripping, shader variants, bundle membership, and LOD availability. Compare reports after Unity upgrades; importer defaults and platform capabilities can change.
Test a clean checkout and batch build. If results depend on a Library cache, local Asset Database state, or an uncommitted ProjectSettings change, the pipeline is not reproducible. Pin package versions and record the editor version used for production.
When a platform override intentionally reduces quality, attach the target-device render and metric evidence. A 1024 texture cap can be correct for one low tier and destructive for readable labels on another. Overrides are product policy, not merely build-size switches.
Run a dependency report before release. A high-resolution source referenced by an Editor-only preview should not leak into the player, and a shared material placed in a broad Addressables group should not pull unrelated products into the initial download.
Unity shipping checklist#
- Tests run in a target player on named device tiers.
- Imported scale, axes, pivots, and bounds match references.
- CPU-readable mesh data is retained only for a documented consumer.
- Mesh and Vertex Compression are treated as different systems.
- Simplification passes silhouette and deformation tests before compression.
- LODGroup thresholds come from screen-relative evidence.
- Frame Debugger explains batch and SetPass behavior.
- Materials, keywords, and instancing are intentional.
- Texture formats and dimensions are verified per platform.
- Animation keys, bone influences, and blend shapes are budgeted.
- Shadows, render scale, and pipeline features share the frame budget.
- Addressable dependency ownership and repeated-load memory remain stable.
Sources and further reading#
- Unity Manual: compressing mesh data
- Unity Manual: Rendering Profiler module
- Unity Manual: Memory Profiler module
- Unity Manual: LOD Group
- Unity Manual: texture compression formats
Unity asset optimization is an import-to-player workflow. Control the data Unity builds, distinguish disk compression from runtime precision, design LOD and materials for the render pipeline, and let target-player profiler evidence decide every tradeoff.