field-guide / getting-art-in
02/10
02task lane

Getting art in

Blender / AI model → in-game, correct scale & facing.

26.07.08e
"A complete Blender-headless → s&box pipeline"

Two-stage flow: Blender headless emits Y-up OBJ, then pure Python writes vmats/vmdls and a C# model catalog.

26.07.22
"A low sun in a street canyon leaves the ground unshadowed"

At single-digit sun elevation a street canyon gets no direct ground light, so nothing on the ground casts a visible shadow while the cascade is billed in full; fix it with sun azimuth, not renderer flags.

26.08.05
"A PointLight born with Shadows = true renders nothing: use a SpotLight"

On current builds a PointLight created with Shadows = true contributes nothing to the frame: no light, no shadow, silently. The six-face cubemap shadow path is broken while SpotLight's single-view path works. For a directional lamp, cast with a downward SpotLight instead.

26.07.22
"A rotated procedural primitive spins about its own origin, not the part pivot"

A primitive authored with a rotation parameter spins about its own object origin, wherever it was placed, not about the part's intended pivot. A raked hood then see-saws about its own middle and shows half the intended wedge in each direction. Rotate the placement coordinate around the pivot first, then place the box there with the same rotation baked in, and remember that a rotated box's own bounding box is trigonometric, not its declared half-length.

26.08.05
"A second render material can seal a doorway shut in the compiled collision"

A PhysicsHullFile node compiled with import_mode HullPerElement treats a material break as an element boundary. Paint two adjoining primitives with a second material and the compiler folds them into one element and builds one convex hull across both. The convex hull of a wall with a hole in it is a solid wall, so every doorway and window built the same way seals shut at once, with no geometry change in the diff. Every offline check stays green, because none of them compile the model. The defect is only visible in the compiled collision, which the editor or an in-engine walk probe can see. Fix by giving physics its own single-material copy of the source mesh, so the render mesh's material list never reaches the physics node, and pin the physics mesh to the render mesh with a lint.

26.07.08e
"Blender OBJ export face order is not deterministic"

bpy.ops.wm.obj_export can reorder face lines across runs on the same mesh: diff the manifest, not the raw OBJ bytes.

26.07.22
"Commit a new .shader together with its compiled .shader_c, or it fails its first load"

A brand-new custom .shader committed without its compiled .shader_c fails its first in-editor load with a misleading 'Invalid Dependency Information' / file-not-found error chain, even when the HLSL is correct. The engine loads shaders only from .shader_c -- commit both.

26.07.08e
"Decimating AI meshes without wrecking them"

Census tris × instances first: silhouette fails before UVs; inject usemtl after scripted OBJ export.

26.07.22
"DirectionalLight.LightColor is a hue control, not a brightness dial"

Scaling DirectionalLight.LightColor does not dim the sun: from 0.001 to 5.0 it renders identically; only Color.Black turns it off. LightColor sets hue, not brightness. A day/night brightness ramp built on it is a silent no-op.

26.07.08e
"Disabling per-instance shadows on ModelRenderer"

ModelRenderer.CastShadows doesn't exist as a settable property: use renderer.SceneObject.Flags.CastShadows instead.

26.07.15a
"For runtime per-surface color, bake a texture: don't rely on complex.shader vertex color"

complex.shader has no shipped source and no evidence it reads vertex color as albedo, so don't plan per-vertex color painting through it. The reliable runtime color-per-surface lane is a baked texture plus Material.CreateCopy and Set("Color", tex).

26.07.08e
"Forge delivery fails but the preview GLB is still downloadable: recover it"

Forge delivery job errors out but the build stage's previewGlbUrl stays downloadable: recover the GLB through Blender into your standard delivery shape instead of re-spending.

26.07.22
"Geometry under an IsStatic root samples its lighting once, at creation"

Geometry parented under an IsStatic root samples its lit appearance at creation time and never resamples. A runtime write to a light component is invisible on that geometry until the world is rebuilt. So a lighting A/B that mutates a light live and re-observes static geometry shows no change, no matter how large the delta. The comparison reads as no effect when the real answer is never re-sampled. Run static-geometry lighting tests through the world build command, not a live component write.

26.07.08e
"High-key chalky sky is capped by two defaults: texture AND tonemapping must both change"

A near-white chalky sky stays mid-grey or blue: fixing the sky texture alone isn't enough because the default tonemapping curve crushes highlights.

26.08.05
"import_rotation fixes the render mesh and leaves PhysicsHullFile collision unrotated"

The render node in a vmdl honours import_rotation. PhysicsHullFile and PhysicsMeshFile ignore it. So correcting an off-axis import with import_rotation fixes the picture and leaves the collision exactly where it was. The model builds clean with no error and no warning, photographs correctly, and a player walks straight through it. Bake a corrected engine-frame copy of the mesh that both the render node and the physics node read, rather than rotating one consumer with a property the other ignores.

26.07.08e
"Importing Kenney / CC0 kits into s&box"

CC0 kits import like any OBJ+vmdl; city kits share colormap.png, nature kits need per-color vmats, and facing is a per-model yaw guess.

26.08.05
"ModelRenderer.Tint above 1.0 wraps a byte and renders dark, not brighter"

ModelRenderer.Tint is packed to eight bits per channel and the cast wraps instead of clamping, so a tint above 1.0 renders dark, not brighter. Rendered luminance follows (tint * 255) mod 256, so 1.02 drops to about 56 and 1.10 reads as 24 of 255. The channels do not wrap together, so a population jittered around 1.05 splits into flat red, yellow, and near-black frames that keep their texture detail and read like an atlas or UV fault. Normalise the authored tint ladder so the largest draw reaches white, and clamp at every sink where a colour becomes a renderer component.

26.07.08e
"My model has no collision even though it loads fine"

RenderMeshFile alone compiles clean with zero physics: ModelCollider needs PhysicsMeshFile/PhysicsHullFile in the vmdl.

26.07.08e
"My model imported upside down / sideways / facing the wrong way"

Blender +X becomes world −Y after OBJ import; yaw +90° faces +X, and Blender +Y rotation tips the +X edge down.

26.07.08e
"My scaled-up tree's collider didn't scale"

CapsuleCollider and ModelCollider ignore WorldScale; BoxCollider follows it. Bake scale into import_scale instead.

26.08.05
"One vmdl, many tagged collision hulls"

A PhysicsShapeList node compiles with several tagged PhysicsHullFile children, so one vmdl can carry a compound collision volume. The per-shape tags stay inert through a scene ModelCollider.

26.07.15a
"Per-shape collision_tags in a vmdl are inert at runtime on a ModelCollider"

A multi-hull vmdl authored with per-shape collision_tags does not get those tags at runtime -- a scene ModelCollider overwrites every shape with the GameObject's own tags, so Scene.Trace.WithoutTags can't filter one hull from another. Split the hulls across separate collider GameObjects instead.

26.07.08e
"s&box units are inches"

1 m = 39.37 engine units: design in SI, convert once at the engine boundary, and audit every consumer.

26.07.08e
"s&box won't load my OBJ/GLB"

Scenes never load raw OBJ/GLB: always wrap with a .vmdl and remap materials under both bare and .vmat names.

26.07.22
"The engine ships render quality profiles that cap your scene, and post-processing is opt-in per camera"

The engine ships its own render quality profiles, backed by real convars, that CAP what your scene asked for. A scene authoring 4 shadow cascades renders 2 for a Low-shadow player. And post-processing (AO, DOF, bloom, tonemapping, SSR) is opt-in per camera: none of it runs until the game adds the component, so a code-created camera renders flat, ungraded, and un-occluded no matter how well the lights are authored.

26.08.05
"The mip chain erases texture detail finer than the object it represents"

A prop that reads as flat, plastic, or untextured almost always has a texture. The mip chain destroyed its contrast before the player saw it, because the detail was authored finer than the scale the surface is seen at.

26.07.15a
"Time-of-day sky swap pops: use a weighted-blend shader instead"

A hard SkyBox2D.SkyMaterial swap produces a visible pop, and stacking two SkyBox2D components with Tint alpha crossfade does not composite. The fix is a single custom sky shader that holds all time-slot textures and blends them by weight.

26.07.08e
"Trajectory preview dots bunch at the apex: sample by arc length, not time"

Even-time samples of a ballistic arc cluster at the slow apex and spread at the fast ends: sample by arc length for evenly spaced preview dots.

26.07.08e
"Unknown shader field? Check the engine's own templates before grepping projects"

Shader field support is unknown and no .shader source exists: check the install's templates/ folder for authoritative syntax instead of grepping sibling projects.

26.07.08e
"Using AI-generated 3D models in a real s&box game"

AI mesh generators (Tripo, Meshy, Rodin) deliver textured OBJ+vmdl, but modeldoc32 headers, ~1m normalize, corrupt textures, bare texture paths, and missing colliders will ERROR the asset until patched.

26.07.08e
"Vector3.Right is (0, -1, 0), not +X"

Source engine convention: Forward = +X, Left = +Y, Right = -Y. Using Vector3.Right for '+X' silently slides geometry the wrong way.

Want to know when new guides or fixes drop? Join the community to help build this out. Report gotchas, flag outdated fixes, or just lurk.

Join the Discord