/// Converts a mesh's vertex positions (and optional colors) into two power-of-two Texture2D assets.
/// Positions are stored as RGBAFloat (xyz1), colors as RGBAFloat.
/// Uses NativeArrays + jobs to fill the textures.
public class MeshToTextures : MonoBehaviour
[Tooltip("Assign your point-cloud mesh here.")]
[Header("Output Settings")]
[SerializeField, Tooltip("Folder (under Assets) where the textures will be saved.")]
private string outputFolder = "Assets/PointCloudTextures";
[SerializeField, Tooltip("Base name for the generated position texture asset.")]
private string positionTextureName = "Positions";
[SerializeField, Tooltip("Base name for the generated color texture asset.")]
private string colorTextureName = "Colors";
[SerializeField, Tooltip("Texture format to use. RGBAFloat is recommended for high-precision point data.")]
private TextureFormat textureFormat = TextureFormat.RGBAFloat;
[SerializeField, Tooltip("Filter mode for the generated textures. For point clouds, Point is often best.")]
private FilterMode filterMode = FilterMode.Point;
[SerializeField, Tooltip("If false, new assets will get unique names instead of overwriting existing ones.")]
private bool overwriteExisting = false;
// ---------------- Jobs ----------------
private struct FillPositionsJob : IJobParallelFor
[ReadOnly] public NativeArray<Vector3> vertices;
public NativeArray<Color> posPixels;
public void Execute(int index)
posPixels[index] = new Color(v.x, v.y, v.z, 1f);
[ContextMenu("Convert Mesh To Textures")]
Debug.LogError("[MeshToTextures] No mesh assigned.", this);
var verticesManaged = sourceMesh.vertices;
if (verticesManaged == null || verticesManaged.Length == 0)
Debug.LogError("[MeshToTextures] Source mesh has no vertices.", this);
var colorsManaged = sourceMesh.colors;
bool hasColors = colorsManaged != null && colorsManaged.Length == verticesManaged.Length;
if (!hasColors && colorsManaged != null && colorsManaged.Length > 0)
$"[MeshToTextures] Color array length ({colorsManaged.Length}) does not match vertex count ({verticesManaged.Length}). Ignoring colors.",
int vertexCount = verticesManaged.Length;
// Power-of-two square texture.
int minSize = Mathf.CeilToInt(Mathf.Sqrt(vertexCount));
int size = Mathf.NextPowerOfTwo(minSize);
int pixelCount = size * size;
NativeArray<Vector3> vertices = default;
NativeArray<Color> posPixels = default;
NativeArray<Color> colPixels = default;
NativeArray<Color> meshColors = default;
// Copy vertices into NativeArray
vertices = new NativeArray<Vector3>(vertexCount, Allocator.TempJob);
vertices.CopyFrom(verticesManaged);
// Allocate pixel buffers (cleared = Color.clear padding)
posPixels = new NativeArray<Color>(pixelCount, Allocator.TempJob, NativeArrayOptions.ClearMemory);
colPixels = new NativeArray<Color>(pixelCount, Allocator.TempJob, NativeArrayOptions.ClearMemory);
// Fill positions via Burst job (no managed loop)
var posJob = new FillPositionsJob
JobHandle posHandle = posJob.Schedule(vertexCount, 64);
// Colors: copy directly if available, rest remain clear
meshColors = new NativeArray<Color>(vertexCount, Allocator.TempJob);
meshColors.CopyFrom(colorsManaged);
NativeArray<Color>.Copy(meshColors, 0, colPixels, 0, vertexCount);
// If you want white instead of clear where points exist, you can do this
// with another job; here we keep it simple and leave colPixels as clear
// for unused pixels and white for used pixels:
using (var white = new NativeArray<Color>(vertexCount, Allocator.TempJob))
for (int i = 0; i < vertexCount; i++)
NativeArray<Color>.Copy(white, 0, colPixels, 0, vertexCount);
// Create textures and push NativeArray data straight in.
var posTex = new Texture2D(size, size, textureFormat, mipChain: false, linear: true)
name = positionTextureName,
wrapMode = TextureWrapMode.Clamp,
var colTex = new Texture2D(size, size, textureFormat, mipChain: false, linear: true)
wrapMode = TextureWrapMode.Clamp,
posTex.SetPixelData(posPixels, 0);
colTex.SetPixelData(colPixels, 0);
// ---------- Asset saving ----------
string folder = string.IsNullOrWhiteSpace(outputFolder)
? "Assets/PointCloudTextures"
: outputFolder.TrimEnd('/', '\\');
if (!Directory.Exists(folder))
Directory.CreateDirectory(folder);
string posPath = Path.Combine(folder, positionTextureName + ".asset");
string colPath = Path.Combine(folder, colorTextureName + ".asset");
posPath = AssetDatabase.GenerateUniqueAssetPath(posPath);
colPath = AssetDatabase.GenerateUniqueAssetPath(colPath);
AssetDatabase.CreateAsset(posTex, posPath);
AssetDatabase.CreateAsset(colTex, colPath);
AssetDatabase.SaveAssets();
$"[MeshToTextures] Power-of-two textures generated ({size} x {size}) from {vertexCount} vertices.\n" +
$"Position texture: {posPath}\n" +
$"Color texture: {colPath}",
if (vertices.IsCreated) vertices.Dispose();
if (posPixels.IsCreated) posPixels.Dispose();
if (colPixels.IsCreated) colPixels.Dispose();
if (meshColors.IsCreated) meshColors.Dispose();