LeafCraft

App Description:

LeafCraft is a completed Mobile App available on Android and iOS that allows you to create intricate and beautiful leaves. You don’t have to be an artist to be able to create something beautiful. Each leaf is unique, and you design it, there are almost no limits to the shape and form of a leaf, you are only bound by your imagination.

This was an independent project, all coding and graphics were done by myself.

Screen Shots:

Development Info:

Engine: Unity

Language: C#

Platform: Android, iOS

Individual project

Google Play Link

iTunes Link

(pending agreement because apple changed their ToS again)

Sample Source Code:

LeafDesigner.cs

This is the master class for the project, most of the important classes are managed and initialized here.

public class LeafDesigner : MonoBehaviour
{
    //manages the contour, veins and sites 
    public LeafBezierManager bezierManager; 
    //encodes the contour, veins and sites into data, passes it to the substance and output b/w image
    public LeafEncoder encoder;
    //colors the leaf
    public LeafColorer colorer;
    //leaf saver
    public LeafSaver saver;
    //leaf _imageSavedr
    public LeafSharer sharer;
    //input manager
    public bool _imageSaved = false;
    public string _imageName;
    public BezierPointHub tip;
    public BezierPointHub bot;
    public List <RectTransform> ButtonRegions;
    public GradientSlider gradientSlider;
    public GameObject ColorCanvas;
    public SkinPopulator texturePopulator;
    public InputField skinNameInput;
    public int MaxSitesLimit = 128 * 128;
    public string FullLeafPath;
    public Color32 [] SubstanceGeneratedTexture;
    public Texture2D coloredImage;
    public Texture2D DataTexture;

    public void DestroyTextures ()
    {
        LeafTextureManager.DestroyTextures ();
    }

    void SetResolutionForPC ()
    {
        if ( Application.platform == RuntimePlatform.WindowsPlayer )
        {
            int ResHight = Mathf.RoundToInt ( Screen.currentResolution.height * 0.85f );
            int ResWidtht = Mathf.RoundToInt ( ResHight * 0.5625f );
            Screen.SetResolution ( ResWidtht, ResHight, false );
        }
    }

    void Initialize ()
    {
        SetResolutionForPC ();
        LeafTextureManager.InitializeTextures ();
        bezierManager = new LeafBezierManager ();
        //create the base leaf shape
        BezierSegment myCurve1 = new BezierSegment ( tip, tip.ChildA, bot.ChildA, bot );
        BezierSegment myCurve2 = new BezierSegment (tip, tip.ChildB, bot.ChildB, bot);
        BezierSpline leftContour = new BezierSpline ( new List<BezierSegment>{myCurve1} );
        BezierSpline rightContour = new BezierSpline ( new List<BezierSegment>{myCurve2} );
        bezierManager.Splines.Add(leftContour);
        bezierManager.Splines.Add(rightContour);
        bezierManager.LeftContour = leftContour;
        bezierManager.RightContour = rightContour;
        bezierManager.TipHub = tip;
        bezierManager.BotHub = bot;
        bezierManager.DeselectAll();
        encoder = new LeafEncoder(this);
        colorer = new LeafColorer(this);
        saver = new LeafSaver(this);
        sharer = new LeafSharer(this);
        this.ColorCanvas = this.gameObject;
        colorer.ColorCanvas = this.ColorCanvas;
        colorer.gradientSlider = this.gradientSlider;
    }

    void Awake ()
    {
        Initialize ();
    }

    void Start ()
    {
    }

    void Update ()
    {
        bezierManager.ManualUpdate();
    }

    public void MakeVeins ()
    {
        bezierManager.MakeVeins ();
    }

    public void MakeTexture ()
    {
        encoder.MakeTexture ();
        _imageSaved = false;
    }

    public void Share()
    {
        Save();
        sharer.Share();
    }

    public void ImageChanged()
    {
        _imageSaved = false;
    }

    public void Save()
    {
        if(_imageSaved == true)
            return;
        FullLeafPath = saver.Save();
        _imageSaved = true;
        Debug.Log("Saving");
        #if UNITY_ANDROID
        if(Application.platform == RuntimePlatform.Android)
        {
            RefreshGalleryWrapper refresh = this.GetComponent<RefreshGalleryWrapper>();
            refresh.RefreshGallery(UniversalImageSaver.PrevImagePathName);
            refresh.RefreshGallery("/storage/emulated/0/DCIM/LeafCraftImages/");
        }
        #endif
    }

    public void EnterColorMode()
    {
        GameModeManager.SetLeafSplineDrawing(false);
        colorer.SetBaseImage(encoder.ConvertSubstanceToPixels());
        gradientSlider.ClearThumbs();
        gradientSlider.MakeThumbsFromGradient(texturePopulator.GetSelectedSkin.GetColorGradient());
        gradientSlider.SetGradientImage();
        gradientSlider.SelectMiddleThumb();
        colorer.ApplyGradient();
        if(Application.platform == RuntimePlatform.WindowsEditor)
            skinNameInput.text = texturePopulator.GetSelectedSkin.SkinName;
    }

    public void SaveLeafSkin()
    {
        if(Application.platform == RuntimePlatform.WindowsEditor)
            texturePopulator.GetSelectedSkin.SkinName = skinNameInput.text;
        texturePopulator.GetSelectedSkin.SetColorGradient(gradientSlider.colorGradient);
        texturePopulator.SaveSkinAsXml(texturePopulator.GetSelectedSkin);
    }

}
LeafBezierManager.cs

This class manages the splines for the leaf. It keeps track of all the splines, and has methods for adding removing and moving splines. It also has methods for raycasting on splines and finding nearest point/control point on spline to a given point.

public class LeafBezierManager
{
    int NumOfSecondaryVeins = 7;
    float BranchAngle = 30;
    float MinPointHubDistance = 0.1f;
    public LeafDesigner leafDesigner { get { return NexusHub.Nexus.leafDesigner; } }
    public Action ShapeChangedAction = () => { };
    public List<BezierSpline> Splines;
    public BezierSpline LeftContour;
    public BezierSpline RightContour;
    public LeafSelectionManager SelectionManager;
    public BezierSpline SelectedSpline{ get { return SelectionManager.SelectedSpline; } }
    public BezierSegment SelectedSegment{ get { return SelectionManager.SelectedSegment; } }
    public BezierPointHub GrabbedPointHub{ get { return SelectionManager.GrabbedPointHub; } }
    public BezierPointHub SelectedPointHub{ get { return SelectionManager.SelectedPointHub; } }
    public BezierSegment GrabbedSegment{ get { return SelectionManager.GrabbedSegment; } }
    public float GrabbedSegmentT{ get { return SelectionManager.GrabbedSegmentT; } }
    public BezierSpline PrimaryVein;
    public BezierPointHub TipHub;
    public BezierPointHub BotHub;
    public VeinFactory veinFactory;
    public SiteManager siteManager;
    public SecondaryVein_00_Curvy secondaryVeins;
    public GameObject bezierPointPrefab{ get { return NexusHub.Nexus.prefabManager.beizerKeyPoint; } }
    InputManager inputManager{ get { return NexusHub.Nexus.inputManager; } }
    GuiManager guiManager{ get { return NexusHub.Nexus.guiManager; } }
    public float BezierPointZ{ get { return TipHub.transform.position.z; } }

    public LeafBezierManager ()
    {
        Initialize ();
    }

    void Initialize ()
    {
        veinFactory = new VeinFactory ( this );
        Splines = new List<BezierSpline> ();
        SelectionManager = new LeafSelectionManager();
        SelectionManager.LeafManager = this;
        siteManager = new SiteManager(this);
    }

    public void DeselectAll ()
    {
        SelectionManager.DeselectAll();	
        foreach ( var spline in Splines )
        {
            foreach ( var segment in spline.Segments )
            {
                segment.Deselect ();
            }
        }
    }

    public void ManualUpdate ()
    {
        if(secondaryVeins != null)
            secondaryVeins.ManualUpdate();
        if ( inputManager.drag && !NexusHub.Nexus.cameraControl.CameraMoving) //something in this if is slow, optimization needed
        {
            MoveGrabbedKeyPoint ();
            MoveGrabbedSegment ();
        }
        if(true)
        {
            foreach ( var spline in Splines )
            {
                spline.UpdatePoints ();
                if(GameModeManager.DrawSplines == true)
                    spline.DrawSpline ();

                if(GameModeManager.DrawHandles == true)
                    spline.DrawHandles ();
            }
        }
        else
        {
            if(GameModeManager.DrawSplines == true)
            {
                LeftContour.DrawSpline();
                RightContour.DrawSpline();
            }
        }
    }

    public void MakeVeins()
    {
        this.MakePrimaryVein();
        if(secondaryVeins == null)
        {
            secondaryVeins = new SecondaryVein_00_Curvy(this);
            secondaryVeins.CheckVeinCount(true);
            secondaryVeins.RemakeAll();
            secondaryVeins.SetFillVeinUpdateAction();
        }
    }

    bool ini16 = false;
    Vector3 [,,] savedData = new Vector3[2,2,4];
    #region buttons
    public void AddPoint (BezierSegment Segment)
    { 
        Vector2 Mid = Segment.Evaluate ( 0.5f );
        GameObject newPoint = GameObject.Instantiate ( bezierPointPrefab, Mid, Quaternion.identity ) as GameObject;
        newPoint.transform.parent = leafDesigner.transform;
        BezierPointHub newKey, newCtrlA, newCtrlB, old0, old1, old2, old3;
        newKey = newPoint.GetComponent<BezierPointHub> ();
        newKey.isDeletable = true;
        newKey.bezierPoint.ClearParents ();
        newCtrlA = newKey.controlPointA.hub;
        newCtrlB = newKey.controlPointB.hub;
        newCtrlA.SetVisible(GameModeManager.DrawControlPoints);
        newCtrlB.SetVisible(GameModeManager.DrawControlPoints);
        old0 = Segment.Point0.hub;
        old1 = Segment.Point1.hub;
        old2 = Segment.Point2.hub;
        old3 = Segment.Point3.hub;
        old0.bezierPoint.ClearParent ( Segment );
        old3.bezierPoint.ClearParent ( Segment );
        //adjust positions of control points
        Vector2 v0, v1, v2, v3;
        v0 = old0.transform.position;
        v1 = old1.transform.position;
        v2 = old2.transform.position;
        v3 = old3.transform.position;
        old1.bezierPoint.Move ( ( v0 + v1 ) / 2f , false);
        newCtrlA.bezierPoint.Move (  ( v0 + 2f * v1 + v2 ) / 4f, false);
        newCtrlB.bezierPoint.Move (  ( v3 + 2f * v2 + v1 ) / 4f, false);
        old2.bezierPoint.Move ( ( v2 + v3 ) / 2f, false);
        //add new segments
        BezierSegment newSeg1 = new BezierSegment ( old0.bezierPoint, old1.bezierPoint, newCtrlA.bezierPoint, newKey.bezierPoint );
        BezierSegment newSeg2 = new BezierSegment ( newKey.bezierPoint, newCtrlB.bezierPoint, old2.bezierPoint, old3.bezierPoint );
        int index = Segment.Index();
        Segment.parentSpline.AddSegmentAt( index, newSeg2);
        Segment.parentSpline.AddSegmentAt( index, newSeg1);
        //delete old segment
        Segment.Delete ();
        SelectionManager.SelectPointHub ( newKey );
        newKey.SetSelectable(true);
        newSeg1.parentSpline.UpdateAttached();
        SelectionManager.SelectPointHub ( newKey );
    }
    
    public void SmoothPoint ()
    {
        if ( SelectedPointHub != null )
        {
            SelectedPointHub.bezierPoint.MakeSmooth ();
        }
    }
    public void CornerPoint ()
    {
        if ( SelectedPointHub != null )
        {
            SelectedPointHub.bezierPoint.MakeCorner ();
        }
    }

    public bool GetSmooth()
    {
        if ( SelectedPointHub != null )
        {
            return SelectedPointHub.isSmooth;
        }
        return false;
    }

    public void ToggleSmooth(bool isSmooth)
    {
        if ( SelectedPointHub != null )
        {
            if(isSmooth && !SelectedPointHub.isSmooth)
                SelectedPointHub.bezierPoint.MakeSmooth ();
            if(!isSmooth && SelectedPointHub.isSmooth)
                SelectedPointHub.bezierPoint.MakeCorner ();
        }
    }

    public void MakePrimaryVein()
    {
        if(this.PrimaryVein != null)
        {
            return;
        }
        veinFactory.MakePrimaryVein ();
        Splines.Add(PrimaryVein);
        this.DeselectAll ();
    }
    #endregion buttons
    
    void MoveGrabbedSegment ()
    {
        if ( GrabbedSegment == null )
            return;
        Vector2 pos = inputManager.MouseWorldPos () + (Vector2) SelectionManager.grabPosition;
        GrabbedSegment.MovePointOnSegment ( GrabbedSegmentT, pos );
        ShapeChangedAction();
    }

    void MoveGrabbedKeyPoint ()
    {
        if ( GrabbedPointHub != null )
        {
            Vector3 targetPos = (Vector3) inputManager.MouseWorldPos () + SelectionManager.grabPosition ;
            GrabbedPointHub.bezierPoint.Move ( GetNearestValidPointPos(targetPos, GrabbedPointHub) );
            ShapeChangedAction();
        }
    }

    Vector3 GetNearestValidPointPos(Vector3 targetPos, BezierPointHub excludedPoint)
    {
        //non key points have no restrictions
        if(!GrabbedPointHub.isKeyPoint)
            return targetPos;
        //This does a check for each point hub and ensures that there is a minimal distance between points
        BezierPointHub nearestPointHub = NearestPointHub ( targetPos,  excludedPoint);
        Vector3 nearestPos = nearestPointHub.bezierPoint.Position;
        if(nearestPointHub == null)
            return targetPos;
        else if((targetPos - nearestPos).magnitude > MinPointHubDistance )
        {
            return targetPos;
        }
        else
        {
            if(targetPos == nearestPos)
            {
                targetPos = targetPos + new Vector3(0f,0.01f,0f);
            }
            return (targetPos - nearestPos).normalized * MinPointHubDistance + nearestPos;
        }
    }

    BezierPointHub NearestPointHub (Vector3 targetPos, BezierPointHub excludedPoint)
    {
        float smallestDistSqrd = Mathf.Infinity;
        BezierPointHub nearestPointHub = null;
        foreach (var pointHub in BezierPointHub.AllPointHubs) 
        {
            if(pointHub == excludedPoint)
                continue;
            if(!pointHub.isKeyPoint)
                continue;
            if(!pointHub.isSelectable)
                continue;
            float dist = (targetPos - pointHub.bezierPoint.Position).sqrMagnitude;
            if(  dist < smallestDistSqrd)
            {
                smallestDistSqrd = dist;
                nearestPointHub = pointHub;
            }
        }
        return nearestPointHub;
    }

    void SetModeSelectable()
    {
    }


    float WorldToScreenDistance ( float worldDistance )
    {
        return worldDistance / ( 2 * Camera.main.orthographicSize ) * Camera.main.pixelHeight;
    }

    float ScreenToWorldDistance ( float ScreenDistance )
    {
        return ScreenDistance * ( 2 * Camera.main.orthographicSize ) / Camera.main.pixelHeight;
    }
}
LeafColorer.cs

Manages the coloring of the leaf by applying a gradient to a grayscale leaf.

public class LeafColorer
{
    public GradientSlider gradientSlider;
    public GameObject ColorCanvas;
    LeafDesigner leafDesigner;
    GradientApplier gradientApplier;
    int [] grayImg;

    Color32 [] cachedGradient;
    public LeafColorer ( LeafDesigner leafDesigner )
    {
        this.leafDesigner = leafDesigner;
        gradientApplier = new GradientApplier ();
    }
    
    public void SetBaseImage (Color32[] unColoredTexture)
    {
        grayImg = gradientApplier.GrayScale ( unColoredTexture );
        gradientSlider.ColorChangeAction += () => {ApplyGradient();};
        gradientSlider.ValueChangedAction += () => {ApplyGradient();};
        if(leafDesigner.coloredImage == null || leafDesigner.coloredImage.width != LeafResolutionManager.CurrentResolutionInt)
        {
            if(leafDesigner.coloredImage != null)
                Texture2D.Destroy(leafDesigner.coloredImage);
            leafDesigner.coloredImage = new Texture2D(LeafResolutionManager.CurrentResolutionInt,LeafResolutionManager.CurrentResolutionInt);
        }
    }
    
    public void ApplyGradient ()
    {
        if(gradientSlider.colorGradient.Gradient != null)
        {
            if(cachedGradient == null ||
               !cachedGradient.ValueEquals(gradientSlider.colorGradient.Gradient))
            {
                cachedGradient = gradientSlider.colorGradient.Gradient;
                Color32 [] pix = gradientApplier.ApplyGradient ( gradientSlider.colorGradient, grayImg );
                leafDesigner.coloredImage.SetPixels32(pix);
                leafDesigner.coloredImage.Apply();
                ColorCanvas.GetComponent<Renderer>().material.mainTexture = leafDesigner.coloredImage;
                leafDesigner.ImageChanged();
            }
        }
    }
}
LeafEncoder.cs

Class for encoding the cell data and metadata for the leaf into a texture.

public class LeafEncoder
{
    LeafDesigner leafDesigner;
    public LeafBezierManager LeafManager { get { return leafDesigner.bezierManager; } }
    public MeshRenderer ColorLeaf;
    public DataTextureEncoder encoder;// = new DataTextureEncoder ();
    float densityToSiteSizeMulti = 2.3f; //2.3 ~ 2.4
    string filePath;
    int ImageSize { get { return LeafResolutionManager.CurrentResolutionInt; } }
    public ProceduralMaterial substance;
    public bool outputContour = true;
    public bool outputSites = true;

    public LeafEncoder ( LeafDesigner leafDesigner)
    {
        Initialize (leafDesigner);
    }

    void Initialize ( LeafDesigner leafDesigner )
    {
        this.leafDesigner = leafDesigner;
        ColorLeaf = this.leafDesigner.GetComponent<MeshRenderer>();
        filePath = Application.dataPath + "../../../GeneratedTextures/";
        encoder = new DataTextureEncoder(leafDesigner);
    }



    public void MakeTexture ( int fileId = 0 )
    {
        DWDebugTimer t1 = StaticNexusReference.Nexus.buttonActionManager.t1;
        Color32[] pixels = new Color32[LeafResolutionManager.CurrentResolutionInt*LeafResolutionManager.CurrentResolutionInt];
        Color32 color = new Color32(0,0,0,255);
        for (int y = 0; y < LeafResolutionManager.CurrentResolutionInt*LeafResolutionManager.CurrentResolutionInt; y++)
        {
            pixels[y] = color;
        }
        LeafTextureManager.ContourTexture.SetPixels32(pixels);
        ContourTextureFiller.Init(this);
        ContourTextureFiller.DrawAndFillContour(); //slowest
        LeafTextureManager.ContourTexture.Apply();
        if(Application.platform == RuntimePlatform.WindowsEditor && outputContour)
        {
            OutputContourTexture(fileId, filePath);
        }
        EncodeSites(fileId);
    }
    
    void EncodeSites(int fileId = 0)
    {
        DWDebugTimer t1 = StaticNexusReference.Nexus.buttonActionManager.t1;
        List<Vector2> siteDots = new List<Vector2>();
        LeafManager.siteManager.GenerateAllSitePoints();//move this somewhere else qwerty
        siteDots = LeafManager.siteManager.SitePoints;
        float SiteSize = LeafManager.siteManager.SiteDensityX * densityToSiteSizeMulti;
        Vector2 v0 = leafDesigner.transform.position;
        Rect texturePosition = new Rect( v0.x - 0.5f, v0.y - 0.5f, 1, 1);
        List<Vector2> coord = DataTextureEncoder.WorldPosToUV(siteDots,texturePosition);
        encoder.CurrentAcceptableDataType = 0;
        encoder.ClearData();
        encoder.ReadInData0 ( coord, SiteSize );
        encoder.MakeDataTexture();
        if(Application.platform == RuntimePlatform.WindowsEditor && outputSites)
        {
            UniversalImageSaver.SaveImageWithPrefix(leafDesigner.DataTexture,"SitesData");

        }
        encoder.PassDataToSubstance(substance);
        encoder.SetSubstanceSeed(substance,DWEasyRandom.GetRandomInt());
        substance.isReadable = true;
        substance.SetProceduralFloat("ImageCalculationSize",(float)LeafResolutionManager.CurrentResolutionLog2);
        substance.cacheSize = ProceduralCacheSize.None;
        substance.RebuildTextures();
    }

    public string shareImgPath = null;
    public void Share()
    {
        Debug.Log("share image");
        if(shareImgPath == null)
        {
            Debug.Log("share image path is null");
            return;
        }
        else
            AndroidHelper.ShareImage(shareImgPath);
    }

    void OutputContourTexture(int fileId = 0, string filePath = "")
    {
        UniversalImageSaver.SaveImageWithPrefix(LeafTextureManager.ContourTexture,"Contour");
    }


    public Color32[] ConvertSubstanceToPixels()
    {
        Texture[] texs = substance.GetGeneratedTextures();
        ProceduralTexture tex = texs[0] as ProceduralTexture;
        leafDesigner.SubstanceGeneratedTexture = tex.GetPixels32(0,0,ImageSize,ImageSize);
        substance.ClearCache();
        return leafDesigner.SubstanceGeneratedTexture;
    }
}
UniversalImageSaver.cs

Class for saving the leaf texture onto hard disk.

public static class UniversalImageSharer
{
    public static void Share ( string filePathName = "", Texture2D texture = null )
    {
        switch (Application.platform) 
        {
        case RuntimePlatform.Android:
            AndroidHelper.ShareImage(filePathName);
            break;
        case RuntimePlatform.WindowsPlayer:
             
            break;
        case RuntimePlatform.WindowsEditor:
             
            break;
        case RuntimePlatform.OSXPlayer:
             
            break;
        case RuntimePlatform.IPhonePlayer:
        {
            texture.OutputToFile("/LeafCraftShare", UniversalImageSaver.IPhonePath);
            StaticNexusReference.Nexus.diffusion.Share("Made with LeafCraft", null, "file://" + UniversalImageSaver.IPhonePath + "/LeafCraftShare.png");
            Debug.Log("PersistentPath: " + UniversalImageSaver.IPhonePath );
            Debug.Log("FB Connected: " + Diffusion.isFacebookConnected() );
        }
            break;
        case RuntimePlatform.WindowsWebPlayer:
             
            break;
        case RuntimePlatform.OSXWebPlayer:
             
            break;
        default:
            break;
        }

    }
}
VeinFactory.cs
public class VeinFactory
{
    GameObject bezierPointPrefab{ get { return StaticNexusReference.Nexus.prefabManager.beizerKeyPoint; } }
    GuiManager guiManager{ get { return NexusHub.Nexus.guiManager; } }
    LeafBezierManager _leafBezierManager;
    public VeinFactory ( LeafBezierManager leafBezierManager )
    {
        _leafBezierManager = leafBezierManager;
    }

    public void MakePrimaryVein ()
    {
        _leafBezierManager.PrimaryVein = MakePrimaryVein ( _leafBezierManager.TipHub, _leafBezierManager.BotHub );
    }

    BezierSpline MakePrimaryVein ( BezierPointHub tip, BezierPointHub bottom )
    {
        GameObject newGO;
        Vector2 mid,vA,vB,v0;
        v0 = tip.transform.position;
        vB = (Vector2)tip.controlPointA.Position - v0;
        vA = (Vector2)tip.controlPointB.Position - v0;
        float angleAtoB = Vector2Extentions.AngleFromAtoB2PI(vA,vB);
        float finalRotation = angleAtoB / 2 + Mathf.PI;
        float magnitude = (vA.magnitude + vB.magnitude) / 2 / 2 / Mathf.PI * angleAtoB ;
        mid = (magnitude * vA.normalized).RotateRad(finalRotation) + v0;
        newGO = GameObject.Instantiate(bezierPointPrefab, tip.transform.position, Quaternion.identity) as GameObject;
        newGO.transform.parent = StaticNexusReference.Nexus.LeafCanvas.transform;
        BezierPointHub veinTip = newGO.GetComponent<BezierPointHub>();
        veinTip.controlPointA.Move(mid);
        veinTip.bezierPoint.Attach(tip.bezierPoint);
        v0 = bottom.transform.position;
        vA = (Vector2)bottom.controlPointA.Position - v0;
        vB = (Vector2)bottom.controlPointB.Position - v0;
        angleAtoB = Vector2Extentions.AngleFromAtoB2PI(vA,vB);
        finalRotation = angleAtoB / 2 + Mathf.PI;
        magnitude = (vA.magnitude + vB.magnitude) / 2 / 2 / Mathf.PI * angleAtoB ;
        mid = (magnitude * vA.normalized).RotateRad(finalRotation) + v0;
        newGO = GameObject.Instantiate(bezierPointPrefab, bottom.transform.position, Quaternion.identity) as GameObject;
        newGO.transform.parent = StaticNexusReference.Nexus.LeafCanvas.transform;
        BezierPointHub veinBottom = newGO.GetComponent<BezierPointHub>();
        veinBottom.controlPointA.Move(mid);
        veinBottom.bezierPoint.Attach(bottom.bezierPoint);
        veinTip.DestroyControlPointB();
        veinBottom.DestroyControlPointB();
        BezierSegment seg = new BezierSegment(veinBottom,veinTip);
        BezierSpline primVein = new BezierSpline(seg);
        primVein.isLoop = false;
        veinTip.SetSelectable(false);
        veinBottom.SetSelectable(false);
        return primVein;
    }

    public BezierSpline MakeHorizontalVein ( BezierPointHub tip, BezierPointHub bottom )
    {
        GameObject newGO;
        BezierSegment segA, segB;
        segA = tip.parentSegmentA;
        segB = tip.parentSegmentB;
        Vector2 mid;
        mid = ( tip.controlPointA.Position +  tip.controlPointB.Position ) / 2f;
        newGO = GameObject.Instantiate(bezierPointPrefab, segA.Evaluate(0.5f), Quaternion.identity) as GameObject;
        newGO.transform.parent = StaticNexusReference.Nexus.LeafCanvas.transform;
        BezierPointHub veinTip = newGO.GetComponent<BezierPointHub>();
        veinTip.controlPointA.Move(mid);
        mid = ( bottom.controlPointA.Position +  bottom.controlPointB.Position ) / 2f;
        newGO = GameObject.Instantiate(bezierPointPrefab, segB.Evaluate(0.5f), Quaternion.identity) as GameObject;
        newGO.transform.parent = StaticNexusReference.Nexus.LeafCanvas.transform;
        BezierPointHub veinBottom = newGO.GetComponent<BezierPointHub>();
        veinBottom.controlPointA.Move(mid);
        veinTip.DestroyControlPointB();
        veinBottom.DestroyControlPointB();
        BezierSegment seg = new BezierSegment(veinTip,veinBottom);
        BezierSpline primVein = new BezierSpline(seg);
        return primVein;
    }

    public void UpdateSecondaryVeinsAndSites( BezierSpline contour, BezierSpline secondaryVeins, BezierSpline siteSpline)
    {
    }

    public BezierSegment MakeSecondaryVein( BezierSegment startSegment, float tStart, BezierSpline contour, float angle )
    {
        BezierSegment contourSeg, newSeg = null;
        return newSeg;
    }

    public void DividePrimarySpline(BezierPointHub tip, BezierPointHub bottom, BezierSpline primary, int numOfChops, out List<BezierSegment> segments, out List<float> roots )
    { //starts from bottom of the spline
        float offsetDistance = 2f;
        segments = new List<BezierSegment>();
        roots = new List<float>();
        Vector2 splineVector = tip.transform.position - bottom.transform.position;
        Vector2 splineVectorIncrement = splineVector / (numOfChops+1);
        Vector2 offsetVector = new Vector2(splineVector.y, -splineVector.x);
        offsetVector.Normalize();
        offsetVector = offsetVector * offsetDistance;

        Vector2 currentRaycastStart = (Vector2)bottom.transform.position + splineVectorIncrement + offsetVector;
        BezierSegment seg;
        float t;
        for ( int i = 0; i < numOfChops; i++ )
        {
            if( primary.RaycastNearest(currentRaycastStart,-offsetVector,out seg,out t))
            {
                segments.Add (seg);
                roots.Add (t);
            }
            currentRaycastStart += splineVectorIncrement;
        }
    }

    public BezierSpline MakeKeyVeins( BezierSpline primary, BezierSpline contour, float veinDistance = 0.1f, float angle = 45f )
    {

        return null;
    }
}
BezierSpline.cs

A collection of BezierSegments. With the above functions extended to the collection.

public class BezierSpline : Selectable
{
    public List<BezierSegment> Segments;
    public bool isSelected = false;
    public bool isLoop = true;
    public List<BezierPoint> attachedPoints = new List<BezierPoint> ();
    public BezierSpline ()
    {

    }
    public BezierSpline ( List<BezierSegment> segments )
    {
        Segments = segments;
        foreach (var segment in segments) 
        {
            segment.parentSpline = this;
        }
    }
    public BezierSpline( BezierSegment segment)
        :this(new List<BezierSegment>{segment})
    {}
    public void UpdatePoints()
    {
        foreach ( var segment in Segments )
        {
            if(segment == null)
            {
                Segments.Remove(segment);
                continue;
            }
            segment.UpdatePoints();
        }
    }

    #region Attachment
    public void UpdateAttached()
    {
        foreach ( var point in attachedPoints )
        {
            point.Position = this.Evaluate(point.attachedSplineT );
        }
    }
    public void AttachPoint(BezierPoint point, float t)
    {
        if(!point.isKeyPoint)
            return;
        point.attachedSpline = this;
        point.attachedSplineT = t;
        this.attachedPoints.Add(point);
    }
    
    public void DetachPoint(BezierPoint point)
    {
        if(this.attachedPoints.Contains(point))
            this.attachedPoints.Remove(point);
        point.attachedSpline = null;
    }
    
    public void DetachAll()
    {
        foreach ( var point in attachedPoints )
        {
            point.attachedSpline = null;
        }
        attachedPoints.Clear();
    }
    #endregion
    public void DrawSpline()
    {
        foreach ( var segment in Segments )
        {
            segment.DrawCurveGLDynamicSegments();
        }
    }
    
    public void DrawHandles()
    {
        foreach ( var segment in Segments )
        {
            segment.DrawHandles();
        }
    }

    public void AddSegment(BezierSegment segment)
    {
        Segments.Add(segment);
        segment.parentSpline = this;
    }
    public void AddSegmentAt( int index, BezierSegment segment)
    {
        Segments.Insert(index,segment);
        segment.parentSpline = this;
    }
    public void RemoveSegment(BezierSegment segment)
    {
        Segments.Remove(segment);
    }

    public void Delete()
    {
        foreach ( var segment in Segments )
        {
            segment.Delete();
        }
    }
    public float ClosestPointOnSplineToPoint(Vector2 point, out float distance, int scanCount = 20, int iterations = 3)
    {
        float leastDistance = float.PositiveInfinity;
        float currentDistance;
        float t = 0;
        float a,b,c;
        float trashT;
        BezierSegment trashSeg;
        float increment;
        a = 0;
        b = 0.5f;
        c = 1;
        for ( int iter = 0; iter < iterations; iter++ )
        {
            increment = (c-a) / (float)scanCount;
            t = a;
            for ( int i = 0; i < scanCount + 1; i++ )
            {
                currentDistance = (Evaluate(t, out trashSeg, out trashT) - point).sqrMagnitude;
                if(currentDistance < leastDistance)
                {
                    leastDistance = currentDistance;
                    b = t;
                }
                t += increment;
            }
            b = Mathf.Clamp( b , 0f , 1f);
            a = Mathf.Clamp( b - increment, 0f , 1f);
            c = Mathf.Clamp( b + increment, 0f , 1f);
        }
        distance = Mathf.Sqrt(leastDistance);
        return b;
    }
    public void GetClosestPointToSpline(Vector2 point, out BezierSegment closestSegment, out float closestT, out float smallestDistance)
    {
        closestSegment = null;
        float distance;
        smallestDistance = float.PositiveInfinity;
        closestT = 0;
        float t;
        foreach ( var segment in Segments )
        {
            if(!segment.IsSelectable)
                continue;
            if(segment == null)
                continue;
            t = segment.ClosestPointOnCurveToPoint(point,out distance,20,2);
            if(distance < smallestDistance)
            {
                smallestDistance = distance;
                closestSegment = segment;
                closestT = t;
            }
        }
    }

    public void SetColor( Color deselectedColor,Color selectedColor,Color doubleSelectedColor)
    {
        foreach (var seg in Segments) 
        {
            seg.DeselectedColor = deselectedColor;
            seg.SelectedColor = selectedColor;
            seg.DoubleSelectedColor = doubleSelectedColor;
            seg.LineColor = deselectedColor;
        }
    }
    
    public bool RaycastNearest( Vector2 rayStart, Vector2 rayDirection, out BezierSegment seg, out float tRoot)
    {
        float root;
        float smallestDist = Mathf.Infinity;
        float dist,closestRoot = 0;
        BezierSegment closestSegment = null;
        foreach (var segment in Segments) 
        {
            if(segment.RaycastNearest(rayStart,rayDirection, out root) )
            {
                dist = (segment.Evaluate(root) - rayStart).sqrMagnitude;
                if(dist < smallestDist)
                {
                    closestSegment = segment;
                    closestRoot = root;
                }
            }
        }
        seg = closestSegment;
        tRoot = closestRoot;
        if(closestSegment == null)
            return false;
        else 
            return true;
    }

    public bool RaycastNearestSplineT( Vector2 rayStart, Vector2 rayDirection, out BezierSegment seg, out float tRoot, out float tSpline)
    {
        tSpline = 0;
        float root;
        float smallestDist = Mathf.Infinity;
        float dist,closestRoot = 0;
        BezierSegment closestSegment = null;
        foreach (var segment in Segments) 
        {
            if(segment.RaycastNearest(rayStart,rayDirection, out root) )
            {
                dist = (segment.Evaluate(root) - rayStart).sqrMagnitude;
                if(dist < smallestDist)
                {
                    closestSegment = segment;
                    closestRoot = root;
                }
            }
        }
        seg = closestSegment;
        tRoot = closestRoot;
        float trash;
        if(closestSegment != null)
            tSpline = ClosestPointOnSplineToPoint(seg.Evaluate(tRoot),out trash);

        if(closestSegment == null)
            return false;
        else 
            return true;
    }

    public void SetSelectable(bool isSelectable)
    {
        IsSelectable = isSelectable;
        foreach ( var seg in Segments )
        {
            if(seg == null)
            {
                continue;
            }
            seg.SetSelectable(isSelectable);
        }
    }
    public override void Select()
    {
        if(!IsSelectable)
            return;
        isSelected = true;
        foreach ( var segment in Segments )
        {
            if(segment.selected == false)//not double selected, for double selected we want the state to continue
                segment.Select();
        }
    }
    public override void Deselect()
    {
        if(!IsSelectable)
            return;
        isSelected = false;
        foreach ( var segment in Segments )
        {
            segment.Deselect();
        }
    }

    public Vector2 Evaluate(float splineT )
    {
        BezierSegment trashSeg;
        float trashT;
        return Evaluate(splineT, out trashSeg , out trashT );
    }

    public Vector2 Evaluate(float splineT, out BezierSegment segment , out float segmentT )
    {
        segment = null;
        segmentT = 0;
        Vector2 pos = Vector2.zero;
        float length = SplineLength();
        splineT = Mathf.Clamp(splineT,0,1);
        if(length == 0)
        {
            Debug.Log("length is 0, unable to Evaluate");
            return pos;
        }
        float targetLength = length * splineT;
        float currentLength = 0;
        for ( int i = 0; i < Segments.Count; i++)
        {
            currentLength += Segments[i].CachedLength;
            if( currentLength >= targetLength )
            {
                segment = Segments[i];
                float targetSegmentLength = targetLength - (currentLength - segment.CachedLength);
                segmentT = targetSegmentLength / segment.CachedLength;
                pos = segment.Evaluate(segmentT);
                return pos;
            }
        }
        Debug.Log("this should not be reached");
        return pos;
    }

    public Vector3 MoveDistanceOnSpline( float startSplineT, out float endSplineT, float targetDistance, float tStepSize = 0.01f )
    {
        float currentT = startSplineT;
        float prevT = startSplineT;
        float currentDistance = 0;
        while(currentT < 1.1f &&
              currentT > -0.1f &&
              currentDistance < targetDistance)
        {
            currentT += tStepSize;
            currentDistance += P2PLineDistance(currentT,prevT);
            prevT = currentT;
        }
        endSplineT = prevT;
        return Evaluate(endSplineT);
    }

    public float P2PLineDistance( float t1, float t2)
    {
        return (Evaluate(t1) - Evaluate(t2)).magnitude;
    }

    public float SplineLength()
    {
        float length = 0;
        foreach ( var segment in Segments )
        {
            length += segment.CachedLength;
        }
        return length;
    }

}

Responsibilities:

  • Programming for entire app.
  • Creation of materials using substance designer
  • Publishing
  • Marketing

Implementation Details:

Outline with Splines

The App starts with a simple outline of a leaf, made of Bezier splines. The user can move control points around, or drag directly on the curve to change the shape. The user can also split the splines to add more segments.

After the user is satisfied with the basic shape, they can progress to the next step where the veins and cell positions are automatically generated. The user can tweak the number and shape of veins and the density of cells.

Vein and Cell Generation

The generation of the veins is not perfect, as it struggles with some concave and weird leaf shapes but should work on most “normal leaf-like shapes”. The algorithm I use can be explained as follows:

First the center vein is generated by connecting the tip and bottom of the leaf with a spline, the control points are set based on the angle of opening of the tip and bottom. This allows the center vein to be curved if the tip and bottom are skewed.

Then perform multiple raycasts from the center vein at fixed step intervals, the direction of the raycasts is a parameter the user can tweak. The raycasts will hit the sides of the leaf, and these are used as control points for the secondary veins.

Lastly the cells are generated similarly to the secondary veins, but instead of drawing lines for the spline, dots at set intervals are drawn along the spline.

Texture

After this step the user chooses a predefined “skin” for the leaf. The skin is a substance, a procedural texture, made in Allegorithmic substance designer. Substances utilize the graphics card so are very fast for making procedural textures. The substance uses a node based approach to generate a texture. There are currently a total of 9 substances I made that the user can choose from.

The App passes the veins as a texture with RGB Channels representing different types of veins, and the Alpha channel as a mask for clipping the leaf. It also passes another texture with the encoded cell data. This data is encoded to the RGBA channels of the texture, using the pixel RGBA channels to store position and size information of the cells. This was done as a workaround to substances not being able to receive large arrays. I later found out that this encoding data to a texture is also used in General-purpose computing on GPUs (GPGPU)

There are some common modules to all the substances I made.

The “DataToCell” module reads in the encoded cell data, and decodes this data into coordinates that are used to make paraboloids that are then converted into Voronoi cells. These look very much like leaf cells.

The “ContourPreprocessor” reads the RGBA texture of the vein texture and separates the different veins and leaf contour mask into their own textures.

With the above data processed, I then create different effects using various image processing techniques to make the final leaves, including noises, image transform matrices, warping, blending, blurring.

Each substance is in effect similar to an advanced image filter in Photoshop, that can be reused and applied to the leaf data generated by the code to create something visually complex and appealing.

Color

After the substance processes the texture, the user can apply a color to the texture.
The texture is initially grayscale, and color is applied using a gradient that the user can define. Each grayscale level is matched with a color on the gradient. This allows for many possibilities in coloring.

Finally the user can save the texture as a PNG or share it on social network apps, including Facebook, Twitter, Instagram.

Postmortem

What Went Well

  • The app did exactly what I envisioned it to do.
  • Code was refactored several times, each time resulting in a cleaner structure.
  • The app was slow at first, was able to speed it up through profiling.

What Went Wrong

  • Using some of the .Net functionality caused problems when porting to Android and iOS, Unity uses a truncated version of .Net for these platforms. I had to refactor and re-implement some of the code due to this.
  • Not a lot of people actually played it.

What I Learned

  • From marketing events and friends I learned that a lot of people really enjoyed the app. All in all I feel like a was a solid app, especially considering it was my first. This did not mean that people would find it and play it. There are many many factors that I learned about.
  • The app market is very saturated, so discoverability is an issue.
  • Maintaining moral is half the battle. Productivity is directly tied with moral, especially on an individual project.