Created
June 5, 2018 08:49
-
-
Save Frooxius/0b2dbd52d846b850ff19c6833541b589 to your computer and use it in GitHub Desktop.
Image Color Distribution Graph (procedural point cloud generator)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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