Meshes
WorldBridge spawn_mesh encodes an SL mesh asset on the server from a JSON geometry file and rezzes it. No viewer
upload, no COLLADA. Most things on the island (stonework, trees, the boat, water ribbons, flagstones, the flash
dome) are meshes built this way by Python.
The input format (*.meshdata.json)
{
"zup": true,
"submeshes": [
{ "rgb": [180, 150, 110],
"verts": [[x, y, z, nx, ny, nz, u, v], ...],
"tris": [[a, b, c], ...] }
],
"skin": { ... } // optional: makes it a rigged animesh (animesh.md)
}
- One submesh is one FACE of the resulting prim (face numbers follow submesh order). A mesh prim has at most 8 faces.
rgb(0..255) is used as the face colour only when notextureis given at spawn.- UVs are optional (6 or 8 numbers per vertex). With UVs, per-vertex UV mapping works (textures.md).
- Without
"zup": truethe input is treated as Y-up and rotated into Z-up as (x, -z, y). - The encoder normalises every vertex to [-0.5, 0.5] against the high mesh's bounding box and carries the real extent as the prim's scale; each submesh gets its own 16-bit quantisation domain.
tools/meshkit.pywrites this format (Mesh.savereturns the bbox centre);tools/parse_uv_obj.jsconverts an OBJ with its own UVs (with azupargument for Z-up exports such as 3ds Max). Usesize=on spawn for a model in arbitrary units (it scales the largest dimension to that many metres).
What the viewer demands of the asset
1. The UV domain key is TexCoord0Domain
The viewer dequantises TexCoord0 using the domain keyed TexCoord0Domain. With the key spelt TexCoordDomain
the viewer finds no UV domain, collapses every UV to one texel, and the whole mesh renders as one flat colour
while the geometry is fine (because PositionDomain was right). Planar mapping still worked, because it ignores
mesh UVs.
- Learned: every textured mesh was a flat grey, magenta or aubergine for weeks. OpenSim's own decoder is lenient about the name, so a server-side "UVs are faithful" round-trip passed. Diffing a real viewer upload found it.
- See:
worldbridge/WorldBridgeModule.cs(HandleSpawnMesh);tools/cmp_mesh_struct.py(lists the submesh keys of a real upload beside yours, which is what found it). - Note:
tools/decode_llmesh_asset.pyandtools/elephant_diag.pystill dequantise UVs with the oldTexCoordDomainkey; they predate the fix.
2. No LLSD notation prefix, and a complete zlib stream
OSDParser.SerializeLLSDBinary(osd) prepends <?llsd/binary?>\n; the viewer's parser rejects the leading <
("Unrecognized character int(60)", then "Mesh header is invalid"). Serialize with SerializeLLSDBinary(osd, false)
for the header and every block. Each block must also be a complete zlib stream with its Adler-32 trailer; a
hand-written 0x78 0x9C plus a raw deflate stream fails the viewer's strict inflate ("Error during mesh LOD
processing. Reason: 6"). Use System.IO.Compression.ZLibStream.
- Learned: both were found in the viewer's own log (
Firestorm.log) after OpenSim's lenient decoder had passed them. Python's strictzlib.decompresshas the viewer's strictness and makes a good byte check.
3. Emit Z-up yourself
The viewer's uploader rights an uploaded mesh's up axis on rez; a programmatic spawn gets no such help. Save with
"zup": true in a Z-up frame (meshkit does).
- Learned: rigged creatures emitted in a Y-up frame lay on their sides.
Geometry that renders
4. Wind every triangle to its intended normal
The viewer back-face culls. Add each face with the normal it is meant to have and let the kit wind it; check()
must return 0 before a spawn.
- Learned: the first waterwheel had half its faces wound inside out ("normals weirdness"); an orientation test box hand-authored with reversed index hacks had half its faces inward.
- See:
tools/meshkit.py(Mesh.quad,Mesh.box,Mesh.check,lathefor knobs and finials).
5. Make stored normals agree with the winding
Faces whose stored normals point against their winding light backwards.
- Learned: an elephant's tusks were "bright underneath, dark on top regardless of sun": inverted normals (their normals disagreed with the winding, -0.15 against +0.93 for the body), not shadow. Recomputing normals from the consistent winding fixed it.
6. Give thin surfaces two sides
A single-plane surface is invisible from behind. Give ears, leaves you can see from both sides, and similar thin parts a thin shell or a second face.
- Learned: the elephant's ears were single planes; a solidify pass fixed them.
7. A face holds at most 65,535 vertices
Indices are 16-bit. Over the limit the viewer draws NONE of that face, and nothing errors.
- Learned: a stone tower's walls (101k vertices on face 0) were simply absent in the viewer while its floor on face 1 drew. The offline raster preview has no such limit, so only the viewer showed it.
- Rule: spread big stonework over several faces with the same texture, each filled to about 60k.
- See:
tools/meshkit.py(saveraises over 65,535),tools/grow_lookout.py(STONE_SUBS).
8. Do not let an empty submesh renumber your faces
meshkit.save drops an empty submesh, and every later face renumbers.
- Learned: a tower's dark core (submesh 7, with submesh 6 unused) uploaded as face 6 and was painted as light stone.
- Rule: fill every submesh or call
save(keep_empty=True)(WorldBridge encodes an empty submesh as NoGeometry).
9. A mesh made and textured in two separate bakes will not fit
The UVs and the texture atlas must come from the same bake run.
- Learned: the elephant rendered as a black void because its mesh UVs and its baked atlas came from different runs, so the UVs pointed almost entirely at empty margin (only the feet had skin). Filling the margin grey only hid it.
- See:
tools/bake_skin.py(unwrap, bake diffuse and normal, export the OBJ in one Blender run). CONFLICTS.md lists the other explanations recorded for the black void.
Placing meshes
10. A mesh object's position is its bounding-box centre
Not the model's origin. meshkit.save returns the bbox centre offset; spawn there. A script that places the mesh
relative to something (a bird's feet on a perch) must carry that offset C and place with feet + C * rot
(tools/build_gull.py).
11. Bake the placement yaw into the vertices
Rotate the geometry (Mesh.bake_yaw) and spawn with identity rotation. Then an llTargetOmega axis or a keyframed
rotation means the same thing in local and region terms (the waterwheel spins true).
12. Geometry can live far from the object's centre
Because the position is the bbox centre, you can put the centre anywhere by adding a speck of invisible geometry on its own face (alpha 0) mirrored to the other side. A rainbow ring 70 m ahead has its centre at the viewer's eye this way, so a script can move it to the eye and turn it. Mind the 256 m prim size limit.
- See:
tools/build_rainbow.py.
13. A new mesh can take seconds to stream into the viewer
A screenshot straight after a spawn can show bare ground where it stands. Check list and a raycast, wait, look
again. A re-uploaded texture has a new UUID; wait about 10 s before judging a blank face.
Cost and reuse
14. Give big meshes real LODs
lods=[medium, low, lowest] meshdata files, normalised against the HIGH mesh's box. Keep the face count (the
encoder pads missing faces with NoGeometry). Without LODs every level is the high mesh, so a tree 60 m away costs
what it does under your nose.
- A grown tree is about 40k triangles at full detail; its lowest level is trunk, main limbs and a few dozen big leaf cards.
- A far LOD must still read as the thing: the tower's lowest level is a stone-faced drum, not its dark core.
15. Instance, do not re-upload
Store a variant's mesh once (spawn a template, keep its meshAsset, despawn it), then spawn_mesh_instance with
sx,sy,sz = the high mesh's bbox extents x scale, and the object centre = root + Rz(yaw) (bbox centre x scale).
The viewer fetches each variant once.
- Give every instance its own shade so a planted wood is not one colour per variant (
tree_year.shade_for). - Judge a variant before planting it many times:
tools/grow_variants.pyrenders every seed on one sheet.
Shapes that recur
16. Tubes (limbs, pipes, ropes) that do not crack, seam or twist
- One ring per JOINT, square to the bisector of the way in and the way out, shared by both segments. A ring per segment end, each square to its own segment, cracks open on the outside of every bend.
- Carry the ring's first spoke along the limb by parallel transport (project the parent's spoke square to the new direction). Picking it fresh from a fixed "up" flips where a limb crosses the threshold and pinches it.
- A ring needs sides + 1 points (the last repeats the first at u = 1), or one strip of every tube has the whole texture squeezed into it backwards.
- Do not mitre a hairpin (space colonisation turns twigs back up to 166 degrees): keep a square ring there and give the continuing segment its own. Seed a side branch's first ring from the CHILD's spoke.
- A frame from
cross(axis, up)is NaN when the axis is vertical: guard it, or a vertical piece makes spawn_mesh return HTTP 400.
17. A flat card vanishes edge-on
Bend it. A gull's wing as arm-up, hand-down (an M) always shows a shape from level.
18. Poses as faces
One mesh can carry every pose's parts on their own faces (body, flight wings, legs, head at rest, head thrown back
calling); a script shows and hides them by face alpha with every face in PRIM_ALPHA_MODE_MASK (crisp, no alpha
sorting, alpha 0 is simply gone). No mesh swapping.
- See:
tools/build_gull.py.
19. Openings in stonework
- Cut the stones at the jambs. Dropping every stone an opening touches leaves a ragged hole far wider than the door.
- Put a dark-grey void panel just behind each opening, and give the core between stones a shadowed-rubble grey. All-black made every joint read as a hole.
- An opening must go THROUGH (a blind recess never shows sky): open the core in height bands at every opening and splay an embrasure behind it (reveals, a sill sloping in, a head sloping up).
- See:
tools/build_cairn.py,tools/build_tower_inside.py.
20. Z-fighting
Two prims whose faces point the same way within about 2.5 mm and overlap shimmer through each other. Eyeballing each joint across a region is hopeless; measure it.
- See:
tools/zfight_audit.py(every box prim's faces as planes;--fixpulls the smaller face 3 mm behind, one-sided; builders callfix(prefix)last). It ignores meshes and joints where an abutting prim already hides the fight. - Learned: an undercroft's walls ran to the south wall's OUTER face because of a sign slip.
Replacing and rebuilding
21. A prim cannot be turned into a mesh by script
llSetPrimitiveParams([PRIM_TYPE, PRIM_TYPE_SCULPT, meshAsset, PRIM_SCULPT_TYPE_MESH]) compiled and was ignored
(sculpt type stayed 0). Replace the object and RE-LAY its life: find the builder that generated its script,
regenerate the script for the new object with the old one's parameters (colour read from list, hard-coded sizes
patched to the new mesh's bbox), then despawn the old one.
22. Spawn the new thing first, then retire the old
- Learned: a failed spawn after the despawn lost a shrine's log.
23. Regrow only from recorded originals
A grown mesh's own bbox is bigger than the prim it replaced; regrowing from it swells the thing every run. Keep an
originals.json beside each grower (assets/boulders/originals.json is one).
24. Builders are idempotent, and they own a name prefix
Builders in tools/ are seeded and re-runnable, and many find their own earlier objects by name prefix and
despawn them before rebuilding. Anything else that shares the prefix is deleted with them.
- Learned:
tools/plant_birds.pydespawns every object whose name starts withbird-, and a replant deleted two hand-placed birds namedbird-static-*. Hand-placed objects got a prefix of their own.
25. Check the site before you build on it
Things placed by an older builder assumed an older level (props floated 16 cm over a new deck), and grown neighbours spread into new footprints (reeds grew up through a new jetty's ramp). Give every new structure a keep-out in every grower that could reach it.
Seeing a mesh before the viewer does
tools/preview_mesh.py: orthographic, painter's-order Lambert silhouettes of meshdata files from a few angles. Judges silhouette and structure only.tools/raster_mesh.py: a z-buffered software rasteriser that honours UVs, textures, tints and the alpha-mask cutoff. It found cracks in a tree trunk the flat preview could not show.- Neither has the viewer's limits (the 65,535 face limit, texture decode rules) or its shading. The viewer is the last word.