A Scene Component that builds a 3D "Minecraft style" cloud layer from a mask texture. It reads a texture's pixels, creates box geometry for opaque pixels with interior face culling, builds a Mesh/Model, and places two SceneObjects that scroll in X to create continuous movement.
using Sandbox;
using System.Collections.Generic;
/// <summary>
/// Builds a 3D Minecraft "fancy" cloud field from a clouds.png-style mask texture.
/// One box per opaque pixel, interior faces culled, scrolled over time.
/// RandomizeStart rolls the texture read to a random pixel offset so the field
/// shows a different slice of the pattern without moving the GameObject.
/// Pair with the clouds.shader material (face-shaded by normal).
/// </summary>
[Title( "Minecraft Clouds" )]
[Category( "Rendering" )]
public sealed class MinecraftClouds : Component
{
[Property, InfoBox("Note: you have to manually drag the GameObject to a desired height in the scene. (On the Z axis)")] public Texture CloudTexture { get; set; }
[Property] public Material CloudMaterial { get; set; }
/// <summary>World units per cloud pixel. MC = 12 blocks ≈ 472u. Lower this for a smaller map.</summary>
[Property, Range( 1f, 4096f )] public float CellSize { get; set; } = 472f;
/// <summary>Cloud layer thickness. MC = 4 blocks ≈ 157u.</summary>
[Property, Range( 1f, 1024f )] public float Thickness { get; set; } = 157f;
/// <summary>Scroll speed along +X (units/sec). 0 = static.</summary>
[Property, Range( 0f, 512f )] public float ScrollSpeed { get; set; } = 24f;
/// <summary>Alpha threshold (0..255) for treating a pixel as cloud.</summary>
[Property, Range( 0, 255 )] public int AlphaThreshold { get; set; } = 127;
/// <summary>Start the field at a random point in the texture (rolls the pattern, no GameObject moving needed).</summary>
[Property] public bool RandomizeStart { get; set; } = true;
/// <summary>Fixed seed for the random start. 0 = different every time.</summary>
[Property] public int Seed { get; set; } = 0;
private Model _model;
private SceneObject _a, _b; // two copies so the field never gaps while scrolling in X
private float _tileWidth;
private Vector3 _basePos;
protected override void OnStart()
{
var goPos = WorldPosition;
goPos.x = 0f;
goPos.y = 0f;
WorldPosition = goPos;
if ( !BuildModel() )
return;
_basePos = Transform.World.Position;
_a = new SceneObject( Scene.SceneWorld, _model );
_b = new SceneObject( Scene.SceneWorld, _model );
UpdatePositions( 0f );
}
protected override void OnUpdate()
{
if ( _a is null ) return;
float offset = ScrollSpeed <= 0f ? 0f : (Time.Now * ScrollSpeed) % _tileWidth;
UpdatePositions( offset );
}
protected override void OnDestroy()
{
_a?.Delete(); _a = null;
_b?.Delete(); _b = null;
}
private void UpdatePositions( float offset )
{
_a.Transform = new Transform( _basePos + new Vector3( offset, 0, 0 ) );
_b.Transform = new Transform( _basePos + new Vector3( offset - _tileWidth, 0, 0 ) );
}
private bool BuildModel()
{
if ( CloudTexture is null || CloudMaterial is null )
{
Log.Warning( "MinecraftClouds: assign both CloudTexture and CloudMaterial." );
return false;
}
int w = CloudTexture.Width;
int h = CloudTexture.Height;
Color32[] px = CloudTexture.GetPixels();
if ( px is null || px.Length < w * h )
{
Log.Warning( "MinecraftClouds: couldn't read pixels. Import clouds.png uncompressed / readable." );
return false;
}
// Roll the texture read to a random starting pixel. clouds.png tiles seamlessly,
// so a wrapped offset is just as valid a cloud field — only the pattern shifts.
int rx = 0, ry = 0;
if ( RandomizeStart )
{
var rng = Seed != 0 ? new System.Random( Seed ) : new System.Random();
rx = rng.Next( 0, w );
ry = rng.Next( 0, h );
}
bool IsCloud( int x, int y )
{
int sx = ((x + rx) % w + w) % w; // wrap (handles -1 at the edges too)
int sy = ((y + ry) % h + h) % h;
var c = px[sy * w + sx];
// alpha mask (vanilla) with a luminance gate (handles opaque white-on-black sheets)
return c.a > AlphaThreshold && (c.r > 10 || c.g > 10 || c.b > 10);
}
var verts = new List<Vertex>( 1 << 17 );
var indices = new List<int>( 1 << 17 );
void Quad( Vector3 a, Vector3 b, Vector3 c, Vector3 d, Vector3 n )
{
Vector3 t = Vector3.Cross( n, Vector3.Up );
if ( t.LengthSquared < 0.001f ) t = Vector3.Forward;
t = t.Normal;
int bi = verts.Count;
verts.Add( new Vertex( a, n, t, new Vector2( 0, 0 ) ) );
verts.Add( new Vertex( b, n, t, new Vector2( 1, 0 ) ) );
verts.Add( new Vertex( c, n, t, new Vector2( 1, 1 ) ) );
verts.Add( new Vertex( d, n, t, new Vector2( 0, 1 ) ) );
indices.Add( bi + 0 ); indices.Add( bi + 1 ); indices.Add( bi + 2 );
indices.Add( bi + 0 ); indices.Add( bi + 2 ); indices.Add( bi + 3 );
}
float cs = CellSize;
float th = Thickness;
// Center the field on the GameObject so clouds extend in every horizontal
// direction (otherwise the GameObject sits at the corner and you see an edge).
float halfW = w * cs * 0.5f;
float halfH = h * cs * 0.5f;
for ( int y = 0; y < h; y++ )
for ( int x = 0; x < w; x++ )
{
if ( !IsCloud( x, y ) ) continue;
float x0 = x * cs - halfW, x1 = x0 + cs;
float y0 = y * cs - halfH, y1 = y0 + cs;
float z0 = 0f, z1 = th;
Vector3 p000 = new( x0, y0, z0 );
Vector3 p100 = new( x1, y0, z0 );
Vector3 p110 = new( x1, y1, z0 );
Vector3 p010 = new( x0, y1, z0 );
Vector3 p001 = new( x0, y0, z1 );
Vector3 p101 = new( x1, y0, z1 );
Vector3 p111 = new( x1, y1, z1 );
Vector3 p011 = new( x0, y1, z1 );
// Top & bottom are always exposed
Quad( p001, p101, p111, p011, Vector3.Up );
Quad( p010, p110, p100, p000, Vector3.Down );
// Sides only where the neighbour cell is empty (face culling)
if ( !IsCloud( x + 1, y ) ) Quad( p100, p110, p111, p101, new Vector3( 1, 0, 0 ) );
if ( !IsCloud( x - 1, y ) ) Quad( p010, p000, p001, p011, new Vector3( -1, 0, 0 ) );
if ( !IsCloud( x, y + 1 ) ) Quad( p110, p010, p011, p111, new Vector3( 0, 1, 0 ) );
if ( !IsCloud( x, y - 1 ) ) Quad( p000, p100, p101, p001, new Vector3( 0, -1, 0 ) );
}
if ( verts.Count == 0 )
{
Log.Warning( "MinecraftClouds: texture produced no cloud cells (check alpha / threshold)." );
return false;
}
var mesh = new Mesh( CloudMaterial );
mesh.CreateVertexBuffer<Vertex>( verts.Count, Vertex.Layout, verts );
mesh.CreateIndexBuffer( indices.Count, indices );
_tileWidth = w * cs;
_model = Model.Builder.AddMesh( mesh ).Create();
Log.Info( $"MinecraftClouds: built {verts.Count / 4} faces from {w}x{h} texture (offset {rx},{ry})." );
return true;
}
}