Scripting Support (C#)

advanced scripting

Voxel Play 4 · Scripting & API

Namespace: VoxelPlay. All classes below live in this namespace. Existing VP3 APIs remain unchanged - this page documents VP4-specific additions and enhancements only.

Getting Started

Add using VoxelPlay; at the top of your script. VP4 APIs use the same patterns as VP3. No additional setup is needed beyond importing the Voxel Play package.

using UnityEngine;
using VoxelPlay;

public class MyScript : MonoBehaviour {
    public ModelDefinition model;

    void Start() {
        // Create a standalone textured GameObject from a model
        GameObject go = VoxelPlayEnvironment.ModelCreateGameObject(model);
        go.transform.position = Vector3.zero;

        // Export the model to OBJ for external tools
        VoxelPlayOBJExporter.ExportModelToOBJ(model, Application.dataPath + "/../Export");
    }
}

OBJ Export

VoxelPlayOBJExporter is a static utility class for exporting voxel models to OBJ format with textures.

ExportModelToOBJ

static string ExportModelToOBJ(
    ModelDefinition md,
    string outputFolder,
    Vector3? scale = null,      // null = Vector3.one
    bool useTextures = true,    // false = geometry only
    string fileName = null      // override output name (in-memory models)
);

Exports a ModelDefinition to .obj + .mtl + PNG textures. Returns the path of the generated OBJ file, or null on failure. Supports regular voxels, microvoxels with dual materials, and rotated model bits.

ExportVoxelToOBJ

static string ExportVoxelToOBJ(
    VoxelDefinition vd,
    string outputFolder,
    Vector3? scale = null,
    bool useTextures = true,
    string fileName = null,         // override output name (in-memory definitions)
    bool? forceAlphaCutout = null   // true = bake alpha cutout regardless of renderType
);

Exports a single VoxelDefinition (including its microvoxels and secondary material) to OBJ.

Model Conversion (Enhanced)

VoxelPlayConverter static methods for generating standalone GameObjects. These existing methods now include full microvoxel texture support with primary/secondary material routing.

GenerateVoxelObject (ModelDefinition)

static GameObject GenerateVoxelObject(
    ModelDefinition md, Vector3 offset, Vector3 scale,
    bool useTextures = true, bool useNormalMaps = true,
    bool usePBR = false, GameObject updateGameObject = null
);

Generates a textured GameObject from a ModelDefinition. Mixed models (regular + microvoxel bits) are packed into a single mesh with a single TextureArray material. Pass an existing GameObject via updateGameObject to update it in place.

GenerateVoxelObject (VoxelDefinition)

static GameObject GenerateVoxelObject(
    VoxelDefinition vd, Vector3 offset, Vector3 scale,
    bool useTextures = true, bool useNormalMaps = true,
    bool usePBR = false, GameObject updateGameObject = null
);

Generates a textured GameObject from a single VoxelDefinition. Microvoxels render with correct primary and secondary textures based on the layout mode (Slabs, TopCap) and per-quad palette routing.

Convenience Wrappers

// On VoxelPlayEnvironment (instance or static)
GameObject ModelCreateGameObject(ModelDefinition md, ...);
GameObject VoxelCreateGameObject(ModelDefinition md, ...);

Delegate to VoxelPlayConverter.GenerateVoxelObject. Signatures unchanged from VP3.

Microvoxel Thumbnails

// MicroVoxels instance method
Texture2D GenerateThumbnail(int width = 256, int height = 256);

Renders a preview image of the microvoxel shape to a Texture2D. Uses a temporary camera and command buffer - no scene objects are affected. Works at runtime and in the editor. The caller is responsible for destroying the returned texture.

PBR Texture Packing

// TextureTools static method
static Texture2D PackSurfaceMap(
    Texture2D metallic, Texture2D smoothness,
    Texture2D occlusion, Texture2D height,
    int resolution = 256
);

Packs four separate PBR channel textures into a single surface map for the VP PBR model shader. Call at runtime or use the editor utility under the VoxelDefinition inspector.

Model Placement

// VoxelPlayEnvironment - enhanced ModelPlace
void ModelPlace(
    Vector3d position, ModelDefinition model,
    ...,
    bool skipEmptyVoxels = false
);

New skipEmptyVoxels parameter. When true, empty positions in the model do not overwrite existing voxels (e.g. water), so models can be placed inside filled volumes without creating air pockets.

Model bits also capture and restore the per-voxel smooth override (ModelBit.smoothOverride): a captured selection keeps which voxels were flipped smooth or cubed, and placing the model applies the override again. See Smooth Terrain.

Biome Lookup

// VoxelPlayEnvironment - new 3-parameter overload
BiomeDefinition GetBiome(float altitude, float moisture, float temperature);

Adds the temperature axis for VP4's 3-axis biome selection. See Biome System Upgrades for details on configuring BiomeZone temperature ranges.

Microvoxel methods

Six new overloads on VoxelPlayEnvironment for placing and destroying microvoxels at an explicit world position, without a VoxelHitInfo or normal-based offset. Designed for procedural generators and world-edit / brush tools. They share the existing MicroVoxelPlace / MicroVoxelDestroy name; C# discriminates by the 2nd parameter type (int = cube, Vector3d = AABB, float = sphere radius).

Cube at position

bool MicroVoxelPlace(Vector3d position, int size, VoxelDefinition voxelType, Color tintColor = default, int rotation = 0);
bool MicroVoxelDestroy(Vector3d position, int size, float probability = 1f);

Places or destroys a cube of size x size x size microvoxels centered on a world position. When microVoxelsSnap is enabled (default) and the cube fits in a single parent voxel, a fast path skips per-microvoxel lookups and just toggles bits.

Axis-aligned bounding box

bool MicroVoxelPlace(Vector3d boxMin, Vector3d boxMax, VoxelDefinition voxelType, Color tintColor = default, int rotation = 0);
bool MicroVoxelDestroy(Vector3d boxMin, Vector3d boxMax, float probability = 1f);

Fills or clears every microvoxel whose center lies inside the given AABB. Bounds are taken as-is (no snap), so callers can paint arbitrary rectangular regions.

Sphere brush

bool MicroVoxelPlace(Vector3d center, float radius, VoxelDefinition voxelType, Color tintColor = default, int rotation = 0);
bool MicroVoxelDestroy(Vector3d center, float radius, float probability = 1f);

Sphere brush. radius is in world units (1 unit = 1 voxel; MicroVoxels.SIZE = 1/16 unit per microvoxel). Microvoxels whose center is within radius of center are placed or removed.

Painting cells

bool MicroVoxelSetColor(Vector3d position, Color32 color);

Paints the microvoxel cell at a world position with a color via the palette system (occupancy is unchanged; a plain full voxel is converted to a full microvoxel grid first, so any solid surface can be painted). Requires Enable Tinting; painted colors persist in saved games. Demo 1 (Earth) binds it to its graffiti mode: press P, then hold the left mouse button and drag to spray. See Colored Microvoxels.

Batch Microvoxel Placement

bool VoxelPlace(List<Vector3d> positions, VoxelDefinition voxelType, List<MicroVoxels> microVoxels, Color tintColor = default, bool refresh = true);

Places the same voxel type on multiple positions, each with its own MicroVoxels data (one entry per position). Affected chunks are refreshed once per batch instead of once per voxel, so this is the right call for stamping code-built props that rasterize a different 16x16x16 shape into each cell. Returns true if every voxel could be placed.

Detail Generators: contract and best practices

Custom detail generators (subclasses of VoxelPlayDetailGenerator) that sculpt content at generation time must follow three rules. They come from how the engine schedules chunk creation and meshing, and breaking them produces bugs that only show up under load.

Register every voxel definition in Init()

public override void Init() {
    env.AddVoxelDefinitions(concrete, masonry, glass, wood);
}

All voxel definitions your generator will ever place must be registered inside Init(), which runs during world initialization, right before the texture atlas is built. Registering definitions later (mid-generation, from AddDetail) forces a texture rebuild while meshing threads may already be iterating the atlas, which can surface as missing textures or thread exceptions. The batch VoxelPlace overloads auto-register unknown definitions as a safety net, but treat that as a fallback, not the design.

Nested AddDetail calls are managed by the engine

If your sculpting calls VoxelPlace or any API that touches a neighbor chunk that does not exist yet, the engine creates that chunk inline. Your generator is never re-entered for it: while a generator is busy inside AddDetail, chunks created this way are queued per generator and processed automatically right after the current call returns, so per-call state (scratch buffers, shared lists) is safe without any guard in your code. If your generator is genuinely re-entrancy safe and you prefer inline processing, set allowNestedExecutions = true on the generator asset to opt out of the deferral.

MicroVoxels ownership: what isShared controls

isShared is the copy-on-write ownership bit of a MicroVoxels instance. While it is false the instance is exclusively owned by the single cell that stores it, and every mutation (digging individual micro voxels, painting, layout or secondary-type changes) edits it in place with zero allocations. This is the fast path and the normal lifelong state for most instances in a world: per-cell unique content, and any clone produced by a previous edit. When it is true the instance is referenced from more than one place, so mutation paths clone it first and the cell keeps the clone, which is again exclusively owned (isShared = false).

The flag is promoted to true automatically when the same instance is stored into a second cell, and definition assets (MicroVoxelsDefinition, model bits) are born shared. The one case where you should still set it manually is a reusable template built in code: automatic promotion only happens on the second placement, so between the first placement and the second the template sits in one cell as exclusively owned, and any mutation reaching that cell (player digging, a merge, a damage pass) would edit your template in place and corrupt every future placement. Creating code-built templates with isShared = true closes that window. See Microvoxels for details and for colored microvoxels.

Smoke Particles

GameObject CreateSmokeParticle(Vector3d position, float scale = 0.35f, float duration = 6f, Color32 color = default);
void CreateSmokeParticles(Vector3d position, int amount, float scale = 0.35f, float duration = 6f, Color32 color = default);

Spawns smoke puffs from the shared particle pool. Puffs ignore gravity and rise with a chaotic wander that widens with age, so a steady emitter produces a column that opens into a cone. Under an opaque ceiling a puff stops rising and drifts horizontally hunting for an opening, re-picking its direction when the path closes, and resumes its ascent once the sky clears - smoke poured into a room escapes through windows or up stairwells by itself.

  • amount (plural form) emits a burst, mirroring the impact particles API; each puff varies its scale and duration.
  • scale is the birth size; puffs swell to roughly 2-3x with an eased-in curve and dissolve at the end of their life.
  • color defaults to smoke grey; its alpha drives transparency (default 178/255). Particles are born blazing yellow and cool through ember red into the smoke color over the first fifth of their life.
  • Puffs collide with opaque geometry but never with each other or with other particles (the particles layer ignores itself).
  • Smoke shares the pool with damage debris (particlePoolSize, default 1000). Heavy use - several persistent emitters - can exhaust it silently; raise the pool size in the environment inspector or from a detail generator's Init().

The calls are one-shot: for a persistent fire, invoke them on a timer from your own component.

Voxel Fires

bool SetVoxelOnFire(Vector3d position, float intensity = 1f, bool light = false);
bool ExtinguishFire(Vector3d position);
bool IsVoxelOnFire(Vector3d position);
int firesCount;
event System.Action<Vector3d> OnFireStarted;
event System.Action<Vector3d> OnFireExtinguished;

A fire is anchored to an existing voxel, which acts as its fuel. While it burns, the engine emits smoke bursts from it (see Smoke Particles): puffs are born blazing yellow with an emissive glow, cool through ember red and rise as a darkening column. intensity scales the size and lifetime of the puffs.

  • Destroy the fuel, kill the fire. Every emission tick re-checks the anchor voxel; if it was destroyed by any means (player, explosion, scripts), the fire extinguishes itself and raises OnFireExtinguished. No special integration is needed in destruction code.
  • SetVoxelOnFire returns false when the position holds no voxel; starting a fire twice on the same voxel is a no-op.
  • light = true makes the fire emit light like a torch (voxel torch lightmap plus a flickering point light) without any torch object; the light is materialized only while the camera is within firesLightRange.
  • ExtinguishFire puts a fire out programmatically (water, extinguishers, rain systems).
  • Fires beyond firesSmokeRange (environment field, default 60) pause their emission but stay alive.
  • The events make gameplay hooks trivial: objectives like "put out all fires", scoring, or audio can subscribe without polling.

Fires are runtime state and are not serialized with saved games yet. Demo 1 (Earth): press I to ignite the targeted voxel, press it again to extinguish.

Deferred Voxel Writes

When a custom terrain or detail generator needs to place voxels into a chunk that may not exist yet (bridges, waterfalls, vegetation or trees that overflow into a neighbour chunk), do not call CreateChunk recursively. Queue the writes instead, and Voxel Play applies them on the main thread once the destination chunk is available.

// Single write
void VoxelPlaceDeferred(Vector3d position, int voxelIndex, VoxelDefinition voxelDefinition,
                              Color32 color, MicroVoxels microVoxels, DeferredVoxelWriteFlags flags,
                              byte waterLevel = 0);

// Batch overload: groups positions by chunk for you (no manual voxel index)
void VoxelPlaceDeferred(List<Vector3d> worldPositions, VoxelDefinition voxelDefinition,
                               Color32 color, MicroVoxels microVoxels, DeferredVoxelWriteFlags flags,
                               byte waterLevel = 0, List<byte> waterLevels = null);
  • position: any world position inside the destination chunk (the chunk is derived automatically).
  • voxelIndex: local voxel index inside that chunk (0 .. chunk voxel count - 1).
  • flags: write conditions (see below); combine with |.

How writes are applied: they are buffered per chunk and drained on the main thread every frame, within the chunk-creation time budget. A chunk's writes are applied once it is populated; with the ForceCreate flag the chunk is created so the writes can land. Writes survive save/load: if a saved chunk is restored later, its pending writes are applied on load. Call these from the main thread (detail-generator callbacks already run on the main thread).

public enum DeferredVoxelWriteFlags {
    None             = 0,
    EmptyOnly        = 1,    // write only if the target voxel is empty
    EmptyOrSame      = 2,    // write if empty or already the same type
    NonSolidOrCutout = 4,    // only overwrite non-solid or cutout voxels
    TopHalf          = 8,    // half-voxel placement into the top half
    MarkExterior     = 16,   // tag the voxel as exterior
    MarkAboveSurface = 32,   // tag the voxel as above the surface
    DisableTrees     = 64,   // prevent tree population on the affected chunk
    ForceCreate      = 128,  // create the destination chunk if it does not exist yet
    SkipModified     = 256,  // skip chunks the player has already modified
    BottomFill       = 512   // also fill the voxel below
}

Pouring Liquids (VoxelPour)

VoxelPour pours a liquid (water or lava) from a source position as a deterministic, one-shot cascade painted through the deferred-write system: a falling column plus a tapered landing pool that spreads up to poolRadius (the level ramps from full at the centre to 1 at the rim) and re-spills at edges into lower tiers. The liquid is not enrolled in the runtime water flood (no spread simulation, no chunk-modified flag), so the result is deterministic and never bloats the save file. It is bounded by maxVoxels, so it is safe to call from a detail generator or on demand.

void VoxelPour(Vector3d source, VoxelDefinition liquid,
                     int poolRadius = 8, int maxVoxels = 2000,
                     System.Func<Vector3d, bool> isSolid = null);
  • source: world position where the liquid originates.
  • liquid: the VoxelDefinition of the liquid to pour (a water or lava voxel).
  • poolRadius (default 8): maximum radius of each landing pool; the level tapers from full at the centre to 1 at the rim.
  • maxVoxels (default 2000): upper bound on the number of voxels painted; caps the cost of the cascade.
  • isSolid: predicate that returns true where the liquid is blocked. Pass a pure positional predicate (terrain density, your own carved geometry) for deterministic generation. Leave it null to test the actual voxel data via GetVoxel - which force-creates chunks at generation time, so during generation call VoxelPour discretionarily (once, guarded), not unconditionally per chunk.

If the falling column meets an existing pool or the sea it merges into it instead of stacking a new pool (the existing water owns that tier). The cascade is painted via VoxelPlaceDeferred, so it lands correctly even across chunks that do not exist yet.

Deferred Torch Placement

For torch-heavy scenes you can register torches without instantiating them up front and let the lighting manager materialize and recycle them by proximity, so only the torches near the camera exist as live objects at any moment.

// Register a torch to be materialized on demand (does not instantiate yet)
void TorchAttachDeferred(Vector3d worldPos, Vector3 normal, ItemDefinition torchDefinition);

// Clear all deferred torches recorded for a chunk (e.g. before re-recording it)
void TorchClearDeferred(Vector3d chunkWorldPos);

// Materialize/recycle deferred torches around an observer (called by the lighting manager)
void ProcessDeferredTorches(Vector3 observerPos);

Use TorchAttachDeferred while generating or decorating the world to record where torches should appear. The lighting manager calls ProcessDeferredTorches as the camera moves, spawning the nearest torches and despawning distant ones, so prefab count, animation and light updates stay bounded regardless of how many torches the world contains.

Public Properties (41.0)

New serialized properties added in 41.0, grouped by area. Each is also available in the corresponding inspector.

Rendering (VoxelPlayEnvironment)

bool enableChunkOcclusionCulling;   // default false
float smoothLightingAOStrength;     // 0-1, default 1
  • enableChunkOcclusionCulling: skips drawing chunks that are sealed off from the camera by solid blocks. Best for caves, tunnels and enclosed interiors; little effect on open terrain. See Chunk Occlusion Culling.
  • smoothLightingAOStrength: strength of the corner darkening (ambient occlusion) baked by smooth lighting - 1 = classic look, 0 = no corner darkening. Exposed as AO Strength under Smooth Lighting. Note: the former Obscurance Mode option is now labeled Sunlight Curve in the inspector (same obscuranceMode property: it shapes the sun light response, not the baked AO).

Torch and Fire Lights (TorchLightAnimator)

float flickerSpeed;     // default 2
float flickerCrackle;   // 0-1, default 0

flickerSpeed controls the smooth base flicker of the point light; flickerCrackle adds harsh firelight on top: random stepped jumps with occasional bright pops and short ember dips (0 = classic smooth pulse). Torches default to the classic pulse; voxel fires use crackle.

Water (WorldDefinition)

bool enableWaterFlood;    // default true
  • enableWaterFlood: lets water voxels spread/flow at runtime. Disable it for static water bodies to avoid runtime re-meshing.

Clouds (WorldDefinition)

VoxelPlayCloudStyle cloudStyle;   // Chunks (default) | Mesh
// Mesh-style parameters:
float cloudMeshMaxHeight;
float cloudMeshHeightVariation;
float cloudMeshAltitudeSpread;
float cloudMeshCoverageMax;
float cloudMeshCoverageSoftness;
float cloudMeshCoverageSpeed;
float cloudMeshSunScatter;

Set cloudStyle = VoxelPlayCloudStyle.Mesh to render clouds as a dedicated mesh with smooth coverage transitions and volume; the cloudMesh* fields tune its height, coverage and sun scattering. Chunks keeps the classic voxel clouds.

Rendering (VoxelDefinition)

bool allowsTextureRotation;   // default true ("Can Rotate" in the inspector)

Controls whether this voxel's textures may be rotated. Side faces rotate by reassigning their texture index; top and bottom faces rotate their UVs. Set to false to lock a voxel's texture orientation.

Biomes (VoxelPlayEnvironment)

bool smartBiomeSurface;   // default true

When enabled, dirt voxels left exposed at the surface are rendered using the biome's surface voxel, so terrain carved or edited at runtime keeps a consistent surface look.

Super Chunks (VoxelPlayEnvironment)

bool enableSuperChunkTrees;

Renders distant, visual-only tree LOD meshes within super chunks. These are rendering-only: they have no real voxels, colliders or gameplay events.

Structural Collapse

The structural collapse system raises events as structures fall and exposes an API to query, damage and manage debris from your own code. See the feature page for the concept and the inspector settings.

Events

All events are on VoxelPlayEnvironment:

EventSignature and when it fires
OnVoxelCollapseBefore(List<VoxelIndex> indices, ref bool cancel) - fires with the voxels that are about to fall, before any debris spawns. Set cancel = true to keep the structure in place.
OnVoxelCollapse(List<VoxelIndex> indices) - the voxels have collapsed and left the grid.
OnCollapseDebrisSpawned(VoxelCollapseDebris debris) - a debris body was created.
OnCollapseDebrisImpact(VoxelCollapseDebris debris, Collision collision, float impactSpeed) - the body hit the world or another body (same speed and cooldown gates as the impact dust). Use it for crush damage or impact SFX.
OnCollapseDebrisDamaged(VoxelCollapseDebris debris, Vector3 hitPosition, int damage, bool cellDestroyed) - a body took damage; cellDestroyed is true when the hit removed a cell.
OnCollapseDebrisSplit(VoxelCollapseDebris source, VoxelCollapseDebris fragment) - a fragment broke off a damaged body.
OnCollapseDebrisRemoved(VoxelCollapseDebris debris, DebrisRemovalReason reason) - a body left the scene. reason is Destroyed, Sunk, SmartRemoved or FellOffWorld.

API

// Pure what-if: would removing the voxel at this position bring anything down?
// Runs the full solver with the cell treated as air. Nothing spawns or changes.
bool WouldCollapse(Vector3d position, List<VoxelIndex> affectedVoxels = null);

// Enumerate the live debris bodies (self-pruning registry). Returns the count.
int GetCollapseDebris(List<VoxelCollapseDebris> results);

// Apply damage to a debris body from a custom weapon; returns true if a cell was destroyed.
bool CollapseDebrisDamage(VoxelCollapseDebris debris, Vector3d hitPoint, int damage, bool addParticles = true);

// Remove a debris body programmatically.
void CollapseDebrisRemove(VoxelCollapseDebris debris, bool addParticles = false);

// Manually trigger a structural collapse around a position (also called internally on destruction).
void VoxelCollapse(Vector3d position, int budget, List<VoxelIndex> voxelIndices = null, float debrisLifetime = 0);

// Blow up an area: damages every voxel within radius and throws a fraction of the destroyed voxels
// outwards as debris pieces (destructible, dust on impact, honoring the world debris removal mode).
// Returns the number of damaged voxels.
int VoxelExplode(Vector3d origin, int damage, int radius, float debrisAmount = 0.5f, float debrisForce = 9f,
    bool attenuateDamageWithDistance = true, bool addParticles = true, bool playSound = false,
    bool canAddRecoverableVoxel = false, List<VoxelIndex> damagedVoxels = null);

WouldCollapse is handy for build-mode warnings ("removing this will drop the roof") or for AI that avoids undermining itself: it treats the target cell as empty and reports whether anything would fall, without spawning debris or firing events.

VoxelExplode is a one-call way to blow up a region: it applies radial damage and turns a fraction of the rubble into flying debris that behaves exactly like collapse debris (you can shoot it apart, it raises dust on impact and it honors the world debris removal mode). Voxel types marked Fragile caught in the blast shatter into particles instead of flying off as pieces.

Was this page helpful?