Skip to content

Instantly share code, notes, and snippets.

@Frooxius
Created June 5, 2018 08:49
Show Gist options
  • Save Frooxius/0b2dbd52d846b850ff19c6833541b589 to your computer and use it in GitHub Desktop.
Save Frooxius/0b2dbd52d846b850ff19c6833541b589 to your computer and use it in GitHub Desktop.
Image Color Distribution Graph (procedural point cloud generator)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BaseX;
namespace FrooxEngine
{
[Category("Assets/Procedural Meshes")]
public class ImageColorDistributionGraph : ProceduralMesh
{
public enum Space
{
RGB,
HSV
}
public readonly AssetRef<Texture2D> Texture;
public readonly Sync<Space> ColorSpace;
public readonly Sync<int> MaxTextureSize;
public readonly Sync<float> BaseSize;
public readonly Sync<float> AccumulateSize;
public readonly Sync<float> MaxSize;
public readonly Sync<float3> Scale;
public readonly Sync<float> AlphaThreshold;
object textureLock = new object();
protected override void OnAwake()
{
// Make sure the base class does its own initialization
base.OnAwake();
// Setup default values, these can be later changed at runtime
ColorSpace.Value = Space.RGB;
// Restrict the maximum texture size, in order to avoid generating excessive number of points
// E.g. 2048*2048 texture wuold be about 4 million points
// 1024*1024 gives maximum of 1 million, which is more acceptable for powerful VR hardware
MaxTextureSize.Value = 1024;
BaseSize.Value = 0.001f;
AccumulateSize.Value = 0.0001f;
MaxSize.Value = 0.01f;
Scale.Value = float3.One;
// All pixels with alpha below this value will be filtered
// The default is just some guesstimated value, where the colors are transparent enough to ignore
AlphaThreshold.Value = 0.2f;
}
protected override IEnumerator<Context> UpdateMesh()
{
// Check if we're actually referencing an asset
if(Texture.Asset == null)
{
// make sure the mesh is empty, in case there's already some generated data
meshx.Clear();
// we're finished
yield break;
}
// lock the texture asset first, to ensure it won't be modified while the data is read
while (!Texture.Asset.TryModificationLock(textureLock))
yield return Context.WaitForNextUpdate();
// lock obtained, store the current parameters locally in case they're changed when generating the mesh as well
var _mode = ColorSpace.Value;
var _baseSize = BaseSize.Value;
var _accumulateSize = AccumulateSize.Value;
var _scale = Scale.Value;
var _maxTexSize = MaxTextureSize.Value;
var _maxSize = MaxSize.Value;
var _alphaThreshold = AlphaThreshold.Value;
// go to background thread to do the heavy processing
yield return Context.ToBackground();
// get the texture data. Using this method ensures that data will be provided even if the texture is not readable
// automatically reloading them into the memory
var tex = Texture.Asset.GetTextureData();
// check if the texture exceeds tha maximum size and get a rescaled copy if it does
// explicitly set the rescaled copy mipmaps to false, since we don't need them, to avoid generating them
if (tex.Size.x > _maxTexSize || tex.Size.y > _maxTexSize)
tex = tex.GetRescaled(_maxTexSize, false);
// Count all the unique colors in the texture
var colorPoints = new Dictionary<float3, int>();
// go through all the pixels of the texture
for(int y = 0; y < tex.Size.y; y++)
for(int x = 0; x < tex.Size.x; x++)
{
var c = tex[x, y];
// it's too transparent, ignore this pixel
if (c.a < _alphaThreshold)
continue;
// just interested in the rgb components, ignore alpha
var rgb = c.rgb;
if (colorPoints.TryGetValue(rgb, out int count))
colorPoints[rgb] = count + 1; // increment the number of pixels with this color
else
colorPoints.Add(rgb, 1); // it is the first of this color, add it to the dictionary
}
// now that we have counted all the colors, we can unlock the texture
Texture.Asset.ModificationUnlock(textureLock);
// the mesh data can be either empty if this is the first run, or can contain data from previous generation
// simply set the number of vertices to the number of unique elements
meshx.SetVertexCount(colorPoints.Count);
// ensure the mesh contains vertex colors and normals (which store encoded size and rotation for the billboard shaders)
// this ensures that the necessary raw arrays will be allocated
meshx.HasColors = true;
meshx.HasNormals = true;
// run through all the points and assign appropriate positions, colors and sizes
int index = 0;
foreach(var p in colorPoints)
{
var rgb = p.Key;
var count = p.Value;
var c = new color(rgb, 1);
// interpret the RGB coordinates as 3D coordinates. Red to X, Green to Y and Blue to Z
// colors usually range from 0 to 1, so simply multiply by the scale to get the final coordinate
float3 xyz;
if (_mode == Space.RGB)
xyz = rgb;
else
{
var hsv = new ColorHSV(c);
xyz = new float3(hsv.h, hsv.s, hsv.v);
}
meshx.RawPositions[index] = xyz * _scale;
// simply assign the color itself as vertex color
meshx.RawColors[index] = c;
// the normals encode the point XY scale and rotation angle (which we ignore)
// let's compute the point size
float size = _baseSize;
// accumulate extra size for each additional point beyond the first one
size += _accumulateSize * (count - 1);
// limit the maximum size
size = MathX.Min(size, _maxSize);
meshx.RawNormals[index] = new float3(size, size, 0);
index++;
}
// vertex data alone isn't enough, we need to have a point submesh so there are primitives that use them
// get a point submesh from the mesh, this will create a new one if it doesn't exist yet
var points = meshx.TryGetSubmesh<PointSubmesh>();
// there will be one point for each vertex. Since they are all laid out sequentially, it's essentially just an array
// of numbers going from zero to the number of points (minus one)
// store the last number of points, so we can fill new data if necessary
int lastCount = points.Count;
// set the new count of points to match the vertices
points.SetCount(colorPoints.Count);
// Simply assign sequential indicies to all the new points (in case the array got expanded)
for (int i = lastCount; i < points.Count; i++)
points.RawIndicies[i] = i;
// We are all done now, the mesh generation is finished and it'll be submitted to the runtime for updating
// and rendering (in case anything is rendering this mesh with appropriate material)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment