Uv Mapping logo

Uv Mapping

Community
kevinpbuckley
uv-mapping

Inspect, generate, transform, and visualize UV channels on StaticMesh assets (UVMappingService). Use when the user asks to check/add/remove UV channels, auto-unwrap UVs, transform (tile/offset/rotate) UVs, check UV health/islands, or export a UV layout image.

Overview

Publisherkevinpbuckley
RepositoryVibeUE
Skill nameuv-mapping
Stars
735
Forks
161
Bundled files
1
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 1 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by kevinpbuckley on GitHub. Read the source before you install it.

Installation

Install the Uv Mapping AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/kevinpbuckley/VibeUE/tree/master/Content/Skills/uv-mapping
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/kevinpbuckley/VibeUE.git /tmp/VibeUE
mkdir -p .claude/skills
cp -r /tmp/VibeUE/Content/Skills/uv-mapping .claude/skills/uv-mapping
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Uv Mapping in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Uv Mapping on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Uv Mapping is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

🧠 Brains complement: IF an unreal-engine-skills-manager tool (external MCP) exists in this session, call it with {action: "load", skill: "meshes-static-and-skeletal"} for UE domain knowledge on this topic — correct APIs, architecture, best practices — and treat it as the rubric for any review / "best practices" question. If no such tool is available (e.g. running under Claude Code or Codex without that MCP), skip this line entirely and proceed with this skill alone — do NOT attempt the call.

UV Mapping Skill

For seams on imported organic assets, first distinguish UV boundaries from split corner normals, image-border wrapping and invalid material-section indices. See the materials skill's references/generated-organic-surfaces.md and the asset-management skill's references/mesh-reimport-materials.md. Do not unwrap an approved mesh merely because its shading has a sharp band.

UVMappingService provides automation-grade UV channel manipulation for StaticMesh assets in UE 5.7+. The service is built around mesh-description editing and post-build commits — every mutation marks the package dirty and rebuilds render data, but does not save. Follow with unreal.EditorAssetLibrary.save_asset(mesh_path) once you finish a batch.

When NOT to Use This Service

UV transforms are the wrong tool for fixing a tiling material that looks bad on a basic shape (cube, sphere, cylinder, cone). Symptoms that say "this is not a UV problem":

  • "The bricks face the wrong way on this face."
  • "The bricks tile fine on the side but look stretched on the cap."
  • "It tiles correctly on one shape but not another with the same material."

These are material-orientation problems, not UV-layout problems. A single 2D UV transform can't satisfy multiple face orientations on a 3D shape — whichever projection you pick, at least one face will be wrong. Reach for one of these instead:

  1. Per-actor child Material Instance with Tiling overridden. If the parent material exposes a Tiling scalar (Megascans M_MS_Srf does), create a child MI with the right multiplier and assign it to the specific actor. The mesh keeps its stock UVs.
  2. World-aligned / triplanar material. A material that samples textures projected from world axes and blends by world normal. Every face on every shape shows correctly oriented bricks at the right size, regardless of mesh UVs. UE provides /Engine/Functions/Engine_MaterialFunctions02/Texturing/WorldAlignedTexture and WorldAlignedNormal for this.

UVMappingService is the right tool for:

  • Generating lightmap UVs / fixing lightmap overlaps
  • Repairing imported broken UVs (no UVs at all, gigantic UVs, scrambled layouts)
  • Adding a detail-texture UV channel that needs different tiling than channel 0
  • Texel-density auditing
  • UV layout export for documentation
  • Channel lifecycle (add / remove / copy / set count)

If the user asks to "fix UVs" on a basic shape with a tiling material, ask whether they want material-side or mesh-side first — defaulting to mesh-side will burn iterations.

Critical Rules

StaticMesh Only (Mostly)

Detailed inspection and all mutating operations target UStaticMesh. SkeletalMesh meshes are accepted by list_uv_channels and mesh_has_uv_channel for read-only channel counts but all other methods will fail on skeletal meshes — the editing path goes through FSkeletalMeshLODModel and is not yet implemented. If you're prompted to UV-map a skeletal mesh, stop and tell the user.

Save After Edits

UVMappingService calls MarkPackageDirty() and PostEditChange() on every successful mutation but does not save. After a batch of edits:

python
unreal.UVMappingService.set_lightmap_settings("/Game/Meshes/SM_Wall", 1, 0, 128, True)
unreal.UVMappingService.generate_lightmap_uvs("/Game/Meshes/SM_Wall", 0, 1, 1.0)
# Save once at the end:
unreal.EditorAssetLibrary.save_asset("/Game/Meshes/SM_Wall")

Health Check Before Editing

Always run get_uv_health first. It is a single call that tells you channel count, lightmap config, overlap status, and texel density per channel. This prevents wasted work like generating lightmap UVs into an index that already has good UVs.

Lightmap Channel Convention

By UE convention:

  • Channel 0 holds the texture/material UVs.
  • Channel 1 is the lightmap UV channel.
  • LightMapCoordinateIndex on the static mesh must point at the lightmap channel.

generate_lightmap_uvs automatically sets LightMapCoordinateIndex = DestUVIndex for you. remove_uv_channel adjusts it if you remove a channel above or at the lightmap.

Channel 0 Cannot Be Removed

remove_uv_channel rejects channel 0 — every static mesh must have at least one UV channel. To replace channel 0's contents, use auto_unwrap_uvs or copy_uv_channel.

MAX_MESH_TEXTURE_COORDS_MD = 8

Static meshes support at most 8 UV channels. add_uv_channel returns failure beyond this limit; set_uv_channel_count rejects counts > 8.

Overlap Detection Is Heuristic

OverlapPercent uses an O(N²) AABB-overlap heuristic on triangle UV bounding boxes, capped at 5,000 triangles. Above the cap, the field returns -1.0 to signal "skipped." It produces conservative false positives (touching but non-overlapping AABBs) — use it as a smoke test, not a precise measurement.

Pack Is Fit-To-Square, Not True Island Packing

pack_uvs rescales the entire channel's UV bounds into [padding, 1-padding]^2. It does not detect connected islands and shelf-pack them. For real island packing, run auto_unwrap_uvs with Box projection first — that produces island-shaped charts the engine packs internally.


Per-Region UV Edits (Artist-Grade)

For meshes where one face/region needs a different UV transform than the rest (e.g. a thin disc whose side strip distorts a wall texture while the cap looks fine), use the per-region transforms. They apply an affine transform to a subset of vertex instances, leaving everything else untouched.

Filter By Vertex Normal Direction

transform_u_vs_by_normal(mesh, lod, channel, axis_x, axis_y, axis_z, min_dot, max_dot, scale_u, scale_v, rotation_degrees, offset_u, offset_v)

Applies the affine transform only to vertex instances whose normal, dotted with the given axis, falls in [min_dot, max_dot]. Common windows for axis = world +Z (0,0,1):

WindowSelects
min_dot=0.5, max_dot=1.0Top (upward-facing)
min_dot=-1.0, max_dot=-0.5Bottom (downward-facing)
min_dot=-0.5, max_dot=0.5Sides (horizontal-pointing)
min_dot=0.7, max_dot=1.0Strict "up" only

count_vertices_by_normal(mesh, lod, axis_x, axis_y, axis_z, min_dot, max_dot) previews the selection without modifying anything — call it first to verify your filter is selecting the right region.

Worked example — fix a thin disc's side strip aspect ratio while leaving the cap correct:

python
# Disc: scale (2.5, 2.5, 0.4), so world dimensions 250cm wide × 40cm tall.
# Side strip is 785cm (circumference) × 40cm (height) — 20:1 aspect.
# Cap is 250×250cm — 1:1 aspect.
# With a uniform brick material at Tiling=4, the cap renders correct bricks
# but the side renders as horizontal pinstripes.

# Step 1: confirm the filter selects only the side
side = unreal.UVMappingService.count_vertices_by_normal(
    "/Game/Meshes/SM_Disc", 0,  0,0,1,  -0.5, 0.5)
print(f"side vertex instances: {side}")  # ~960 for engine cylinder

# Step 2: scale U on the side only by 20x to bring tile aspect back to ~1:1
unreal.UVMappingService.transform_u_vs_by_normal(
    "/Game/Meshes/SM_Disc", 0, 0,
    0,0,1,        # axis = world Z
    -0.5, 0.5,    # side selector
    scale_u=20.0, scale_v=1.0,  # only U
)
unreal.EditorAssetLibrary.save_asset("/Game/Meshes/SM_Disc")

💡 Which axis to scale: figure out the world-space aspect of the misbehaving region, then scale UVs to compensate. If a region is 20× wider than tall in world, scale U by 20× to make the brick texture tile 20× more in U than V — bricks now appear correctly proportioned.

UV Islands (Most Robust Per-Region Filter)

Use identify_uv_islands + transform_uv_island when normal-direction or polygon-group filters can't cleanly separate the regions you want to operate on (e.g. an irregular mesh, or a UV layout where similar-normal faces are still distinct islands).

identify_uv_islands(mesh, lod, channel) returns an array of FUVIslandInfo, one per connected UV island. Two vertex instances are in the same island iff they're reachable through triangles with no UV seam between them.

python
islands = unreal.UVMappingService.identify_uv_islands("/Game/Meshes/SM_Disc", 0, 0)
for i in islands:
    n, c = i.average_normal, i.world_center
    print(f"Island {i.island_id}: tris={i.triangle_count} VIs={i.vertex_instance_count} "
          f"bounds=({i.min_u:.2f},{i.min_v:.2f})..({i.max_u:.2f},{i.max_v:.2f}) "
          f"normal=({n.x:.2f},{n.y:.2f},{n.z:.2f}) center=({c.x:.0f},{c.y:.0f},{c.z:.0f})")

Each island carries its own bounds, area, average world-space center and average normal — use those fields to identify which island is which. For a simple cylinder you'll get 3 islands: top cap (normal ≈ +Z), bottom cap (normal ≈ -Z), and the side strip (largest triangle count).

Once you've identified the island, transform just it:

python
# Example: scale only the side strip's UV U by 4x — caps and other islands untouched
side = max(islands, key=lambda i: i.triangle_count)
unreal.UVMappingService.transform_uv_island(
    "/Game/Meshes/SM_Disc", 0, 0, side.island_id,
    scale_u=4.0, scale_v=1.0)

Island ids are stable as long as the mesh's UV topology hasn't changed since identify_uv_islands was called. If you re-run an auto_unwrap / pack / structural change, call identify_uv_islands again for fresh ids before transforming.

Filter By Polygon Group (Material Slot)

transform_u_vs_by_polygon_group(mesh, lod, channel, polygon_group_name, scale_u, scale_v, rotation_degrees, offset_u, offset_v)

Applies the transform only to triangles in the named polygon group (typically a material slot). Use when the mesh has clean per-section slots — e.g. a wall with separate body and trim material slots, where you want different tiling per slot but the same base material.

list_polygon_groups(mesh, lod) returns the slot names available on a LOD. The engine basic shapes have just one slot (DefaultMaterial), so this filter is most useful on imported meshes with authored slots.

python
groups = unreal.UVMappingService.list_polygon_groups("/Game/Meshes/SM_Wall", 0)
# ['Body', 'Trim']
unreal.UVMappingService.transform_u_vs_by_polygon_group(
    "/Game/Meshes/SM_Wall", 0, 0, "Trim",
    scale_u=2.0, scale_v=2.0,  # 2x denser bricks on trim only
)

Common Mistakes to Avoid

WRONGCORRECT
result.success returning False but no error stringCheck result.message — every failure includes a reason
Generating lightmap UVs without setting MinChartSpacingPercentPass at least 1.0 for typical meshes; smaller values pack tighter but risk bleeding
Calling transform_uvs on the lightmap channelAvoid — transforms break the channel's [0,1] packing. Use it on material UV channels only
Forgetting to save after editsAdd unreal.EditorAssetLibrary.save_asset(...) after the batch
Editing UVs on a skeletal meshNot supported — only StaticMesh works
Setting LightMapCoordinateIndex past the channel countUse set_uv_channel_count first, or add_uv_channel

Return Types — Exact Properties

FUVMappingResult (every mutating call)

PropertyTypeDescription
successboolTrue iff the operation completed
mesh_pathstrEchoes the input path
messagestrFailure reason or success summary

FUVChannelInfo (list_uv_channels, get_uv_channel_info)

PropertyTypeDescription
lod_indexintLOD this channel belongs to
channel_indexintUV channel (0..7)
vertex_instance_countintNumber of vertex instances
triangle_countintNumber of triangles
min_u, min_v, max_u, max_vfloatUV bounding box
overlap_percentfloat% of triangles with overlapping UV AABB. -1 if mesh too large (>5000 tris)
in_unit_square_percentfloat% of vertex UVs inside [0,1]^2
texel_density1kfloatAvg texels/UE-unit at 1024px reference

FUVHealthReport (get_uv_health)

PropertyTypeDescription
mesh_pathstr
lod_countint
lightmap_coordinate_indexintThe channel lightmaps sample at runtime
light_map_resolutionintPer-mesh lightmap texel size
b_generate_lightmap_uvsboolLOD 0 BuildSettings flag
channelslist[FUVChannelInfo]Per-LOD per-channel stats
b_lightmap_has_overlapsboolTrue if the lightmap channel has any AABB overlap
warningslist[str]Human-readable warnings

FUVLightmapSettings (get_lightmap_settings)

PropertyTypeDescription
b_generate_lightmap_uvsboolLOD 0 BuildSettings flag
source_lightmap_indexintUV channel the generator reads seams from
destination_lightmap_indexintUV channel the generator writes into
lightmap_coordinate_indexintRuntime sample channel
light_map_resolutionintMesh-level lightmap resolution
min_lightmap_resolutionintMin allowed lightmap resolution

Workflows

Workflow: Generate Lightmap UVs for a Newly Imported Mesh

python
import unreal

mesh = "/Game/Meshes/SM_Wall"

# 1. Health check
ok, health = unreal.UVMappingService.get_uv_health(mesh)
print(f"Channels found: {len(health.channels)}, lightmap idx: {health.lightmap_coordinate_index}")

# 2. Generate the lightmap channel (creates channel 1 if needed, sets LightMapCoordinateIndex=1)
result = unreal.UVMappingService.generate_lightmap_uvs(
    mesh,
    source_uv_index=0,
    dest_uv_index=1,
    min_chart_spacing_percent=1.0,
)
print(f"GENERATED: {result.message}")

# 3. Configure lightmap resolution (256 for hero props, 64 for background)
unreal.UVMappingService.set_lightmap_settings(mesh, 1, 0, 128, True)

# 4. Save
unreal.EditorAssetLibrary.save_asset(mesh)

# 5. Verify
ok, health = unreal.UVMappingService.get_uv_health(mesh)
assert not health.b_lightmap_has_overlaps, "Lightmap still has overlaps after generation"

Workflow: Add a Detail-Texture UV Channel and Tile It

python
import unreal

mesh = "/Game/Meshes/SM_Wall"

# Add channel 2 if not present
if not unreal.UVMappingService.mesh_has_uv_channel(mesh, 0, 2):
    r = unreal.UVMappingService.add_uv_channel(mesh, 0)
    print(f"ADDED: {r.message}")

# Copy material UVs (channel 0) into channel 2 as a starting point
r = unreal.UVMappingService.copy_uv_channel(mesh, 0, 0, 2)
print(f"COPIED: {r.message}")

# Tile by 4 (scale UVs)
r = unreal.UVMappingService.transform_uvs(mesh, 0, 2, scale_u=4.0, scale_v=4.0)
print(f"TILED: {r.message}")

unreal.EditorAssetLibrary.save_asset(mesh)

Workflow: Auto-Unwrap a Mesh That Imported with Bad UVs

python
import unreal

mesh = "/Game/Meshes/SM_RuinPiece"

# Box projection produces clean per-face charts on chunky props
r = unreal.UVMappingService.auto_unwrap_uvs(
    mesh, lod_index=0, channel_index=0,
    projection_type="Box",
    hard_angle_threshold=66.0,
)
print(f"UNWRAPPED: {r.message}")

# Pack the result tightly
r = unreal.UVMappingService.pack_uvs(mesh, 0, 0, padding_percent=1.0)
print(f"PACKED: {r.message}")

# Regenerate the lightmap channel from the new charts
unreal.UVMappingService.generate_lightmap_uvs(mesh, 0, 1, 1.0)

unreal.EditorAssetLibrary.save_asset(mesh)

Workflow: Visual UV Inspection (AI-Friendly)

python
import unreal, os

mesh = "/Game/Meshes/SM_Wall"
out = os.path.join(os.environ["TEMP"], "uv_layout.png")

r = unreal.UVMappingService.export_uv_layout_image(
    mesh, lod_index=0, channel_index=1, output_path=out, image_size=1024)
print(f"EXPORTED: {r.message}")
# Then read the PNG back to inspect it visually (Read the file path directly,
# or surface it through the engine's screenshot/image toolset).

Workflow: Batch Lightmap Pass on All Meshes in a Folder

python
import unreal

results = unreal.EditorAssetLibrary.list_assets("/Game/Meshes/Architecture")
for asset in results["assets"]:
    if asset["asset_class"] != "StaticMesh":
        continue
    path = asset["asset_path"]

    ok, health = unreal.UVMappingService.get_uv_health(path)
    if health.b_lightmap_has_overlaps or not health.b_generate_lightmap_uvs:
        unreal.UVMappingService.generate_lightmap_uvs(path, 0, 1, 1.0)
        unreal.UVMappingService.set_lightmap_settings(path, 1, 0, 128, True)
        unreal.EditorAssetLibrary.save_asset(path)
        print(f"FIXED: {path}")
    else:
        print(f"OK:    {path}")

Property Formats

Projection Types (auto_unwrap_uvs)

String, case-insensitive:

  • "Planar" — projects along world +Z. Best for flat panels.
  • "Box" — six-axis projection with seams along sharp edges. Best general-purpose unwrap.
  • "Cylindrical" — wraps around world Z axis. Best for tubes / columns.

Min Chart Spacing (generate_lightmap_uvs)

Float, percent of UV space (0.5..2.0 typical):

  • 0.5 — tight packing, risk of bleeding at low lightmap resolution
  • 1.0 — default, safe for 64–128 lightmap resolution
  • 2.0 — generous, good for 32 resolution background props

Lightmap Resolution (set_lightmap_settings)

Power of two preferred:

  • 32–64 — background / non-hero
  • 128 — typical world geometry
  • 256–512 — hero props, large architecture

Verification After Edits

For lightmap-related changes, always re-run get_uv_health after the save to verify:

python
ok, health = unreal.UVMappingService.get_uv_health(mesh)
assert ok
assert not health.b_lightmap_has_overlaps
assert health.lightmap_coordinate_index == 1

For unwrap operations, export the layout PNG and visually inspect it (read the PNG back, or surface it through the engine's screenshot/image toolset) — the cheapest way to confirm the unwrap looks reasonable.

Sample scripts (run via execute_python_code)

  • scripts/inspect_uvs.txt — inspect UV channels/health and export a UV layout image.

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Uv Mapping AI skill do?

Inspect, generate, transform, and visualize UV channels on StaticMesh assets (UVMappingService). Use when the user asks to check/add/remove UV channels, auto-unwrap UVs, transform (tile/offset/rotate) UVs, check UV health/islands, or export a UV layout image.

Why use Uv Mapping on TypingMind?

Because you install it once and use it with any model. Uv Mapping is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Uv Mapping in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/kevinpbuckley/VibeUE/tree/master/Content/Skills/uv-mapping. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Uv Mapping?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Uv Mapping?

As many as you like. As long as a model supports skills, you can use Uv Mapping with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Uv Mapping AI skill free?

Yes. It is published on GitHub by kevinpbuckley under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇