/// <summary>
    /// Creates cookies as Texture2Ds, used when performing keystone (pixel-by-pixel fallback)
    /// </summary>
    /// <param name="precalculatedData"></param>
    /// <param name="precalculatedCookie"></param>
    /// <param name="imageToProject"></param>
    public Cookie(ProjectedImageInCookieData precalculatedData, Texture precalculatedCookie, Texture imageToProject)
    {
        // this contructor is only used in pixel-by-pixel mode
        pixelByPixel = true;

        projectedImage   = imageToProject;
        projectedImage2D = (Texture2D)projectedImage;
        if (projectedImage2D != null)
        {
            imageColours = projectedImage2D.GetPixels32();
        }
        else
        {
            imageColours = null;
        }

        projectedImageData = precalculatedData;
        if (projectedImageData.colour)
        {
            imageType = ImageType.Colour;
        }

        // create cookies
        redCookie          = new Texture2D(precalculatedData.textureSize, precalculatedData.textureSize, TextureFormat.Alpha8, false);
        redCookie2D        = (Texture2D)redCookie;
        redCookie.wrapMode = TextureWrapMode.Clamp;
        // copy blacks from precalculated cookie
        Graphics.CopyTexture(precalculatedCookie, redCookie);

        redColors = redCookie2D.GetPixels32();

        if (imageType == ImageType.Colour)
        {
            greenCookie          = new Texture2D(precalculatedData.textureSize, precalculatedData.textureSize, TextureFormat.Alpha8, false);
            blueCookie           = new Texture2D(precalculatedData.textureSize, precalculatedData.textureSize, TextureFormat.Alpha8, false);
            greenCookie2D        = (Texture2D)greenCookie;
            blueCookie2D         = (Texture2D)blueCookie;
            greenCookie.wrapMode = blueCookie.wrapMode = TextureWrapMode.Clamp;
            Graphics.CopyTexture(precalculatedCookie, greenCookie);
            Graphics.CopyTexture(precalculatedCookie, blueCookie);
            greenColors = greenCookie2D.GetPixels32();
            blueColors  = blueCookie2D.GetPixels32();
        }

        UpdateCookie(false);
    }
    /// <summary>
    /// Calculates where the image will be in the cookie and creates the whole cookie texture
    /// </summary>
    void UpdateCookie(bool doBlacks = true)
    {
        if (pixelByPixel)
        {
            if (redColors == null) // have we already processed?
            {
                return;
            }

            if (doBlacks)
            {
                float resultWidth, resultHeight;
                resultWidth  = distance / data.ratio;
                resultHeight = resultWidth / data.aspect;

                // calculate image position in pixels

                // calculate shift in meters
                Vector2 shift = new Vector2(resultWidth * (data.shift_h / 200.0f),
                                            resultHeight * (data.shift_v / 200.0f));

                // position of image in meters, relative to projector centre
                float imageLeftMeters = maxImageEdgeDistance - (resultWidth / 2.0f) + shift.x;
                float imageTopMeters  = maxImageEdgeDistance - (resultHeight / 2.0f) + shift.y;

                // posisiton of image in the cookie texture
                int imageLeftPixels = (int)(imageLeftMeters * metersToPixels);
                int imageTopPixels  = (int)(imageTopMeters * metersToPixels);

                // size of image in the cookie texture
                int imageWidthPixels  = (int)(resultWidth * metersToPixels);
                int imageHeightPixels = (int)(resultHeight * metersToPixels);

                // keystone width
                float keystoneH = data.keystone_h / 100f;
                if (keystoneH < 0)
                {
                    keystoneH *= -1;
                }
                float keystoneMinWidth = imageWidthPixels * (1f - keystoneH);

                float keystoneV = data.keystone_v / 100f;
                if (keystoneV < 0)
                {
                    keystoneV *= -1;
                }
                float keystoneMinHeight = imageHeightPixels * (1f - keystoneV);

                // data about where the image is located in the cookie
                projectedImageData = new ProjectedImageInCookieData(imageLeftPixels, imageTopPixels, imageWidthPixels, imageHeightPixels,
                                                                    keystoneMinWidth, keystoneMinHeight,
                                                                    data.keystone_h < 0, data.keystone_v < 0,
                                                                    imageType == ImageType.Colour, textureSize);
            }

            PixelCalcArgs args;
            if (projectedImage)
            {
                args = new PixelCalcArgs(projectedImage.width, projectedImage.height, imageType, doBlacks, 0, redColors.Length);
            }
            else
            {
                args = new PixelCalcArgs(0, 0, imageType, doBlacks, 0, redColors.Length);
            }
            CalculatePixels(args);

            // apply new colours once cookies are calculated
            redCookie2D.SetPixels32(redColors);
            redCookie2D.Apply();
            if (imageType == ImageType.Colour)
            {
                greenCookie2D.SetPixels32(greenColors);
                greenCookie2D.Apply();
                blueCookie2D.SetPixels32(blueColors);
                blueCookie2D.Apply();
            }
        }
        else // shader-based approach
        {
            // safety nets
            if (projectedImage == null) // should never happen, as we create a white image when null is passed in
            {
                Debug.Log("ProjectedImage is null!");
                return;
            }
            if (projectedImage as Texture2D != null)
            {
                if (((Texture2D)projectedImage).format != TextureFormat.RGBA32)
                {
                    Debug.Log("Texture " + projectedImage.name + " is not correct TextureFormat RGBA32");
                    return;
                }
            }
            if (projectedImage as RenderTexture != null)
            {
                if (((RenderTexture)projectedImage).format != RenderTextureFormat.ARGB32)
                {
                    Debug.Log("RenderTexture " + projectedImage.name + " is not correct RenderTextureFormat ARGB32");
                    return;
                }
            }

            // imageWithBorder is a full-size colour copy of the original image, but with a 1px black border to enable the lens shift effect
            Graphics.CopyTexture(projectedImage, 0, 0, 0, 0, projectedImage.width, projectedImage.height, imageWithBorder, 0, 0, blackBorderSize, blackBorderSize);

            // apply the shift in the cookie
            Graphics.Blit(imageWithBorder, imageShifted, cookieSpaceScale, cookieSpaceOffset);

            // split the shifted image into its 3 channels
            Graphics.Blit(imageShifted, (RenderTexture)redCookie, stripRed);
            Graphics.Blit(imageShifted, (RenderTexture)greenCookie, stripGreen);
            Graphics.Blit(imageShifted, (RenderTexture)blueCookie, stripBlue);
        }

        // garbage collection, otherwise RAM usage goes waaaaaaay up
#if UNITY_EDITOR
        if (EditorApplication.isPlaying)
        {
#endif
        // deallocate memory now that cookie has been created
        if (projectedImage as Texture2D != null)
        {
            projectedImage = null;
        }
        imageColours = null;
        if (!supportLiveUpdate)
        {
            redColors = greenColors = blueColors = null;
        }
#if UNITY_EDITOR
    }
#endif
    }