/// <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
    }
    void CalculatePixels(PixelCalcArgs args)
    {
        int   x, y, pi_x, pi_y;
        int   rowWidth, colHeight;
        float f;
        float rowProgress, colProgress;

        byte red, green, blue;

        red = green = blue = 255;

        // set pixel colors
        for (int i = 0; i < args.endIndex; i++)
        {
            x = i % projectedImageData.textureSize;
            y = i / projectedImageData.textureSize;

            // if no H keystone, make row width constant
            if (projectedImageData.imageWidthInCookie == projectedImageData.keystoneMinWidth)
            {
                rowWidth = projectedImageData.imageWidthInCookie;
            }
            else
            {
                // calculate row width after keystone correction
                float verticalProgress = (y - projectedImageData.imageTopEdgeInCookie) / (float)projectedImageData.imageHeightInCookie;
                if (projectedImageData.keystoneH_flip)
                {
                    verticalProgress = 1f - verticalProgress;
                }
                rowWidth = Mathf.RoundToInt(Mathf.Lerp(projectedImageData.imageWidthInCookie, projectedImageData.keystoneMinWidth, verticalProgress));
            }
            rowProgress = (x - (projectedImageData.imageCentreH - rowWidth / 2)) / (float)rowWidth;

            // if no V keystone, make col height constant
            if (projectedImageData.imageHeightInCookie == projectedImageData.keystoneMinHeight)
            {
                colHeight = projectedImageData.imageHeightInCookie;
            }
            else
            {
                // calculate column height after keystone correction
                float horizontalProgress = (x - projectedImageData.imageLeftEdgeInCookie) / (float)projectedImageData.imageWidthInCookie;
                if (projectedImageData.keystoneV_flip)
                {
                    horizontalProgress = 1f - horizontalProgress;
                }
                colHeight = Mathf.RoundToInt(Mathf.Lerp(projectedImageData.imageHeightInCookie, projectedImageData.keystoneMinHeight, horizontalProgress));
            }
            colProgress = (y - (projectedImageData.imageCentreV - colHeight / 2)) / (float)colHeight;

            // inside the image row?
            if (y > projectedImageData.imageCentreV - colHeight / 2 && y < projectedImageData.imageCentreV + colHeight / 2 &&
                // inside the image column?
                x > projectedImageData.imageCentreH - rowWidth / 2 && x < projectedImageData.imageCentreH - 1 + rowWidth / 2)
            {
                // white if no projected image
                if (imageColours == null)
                {
                    redColors[i] = new Color32(255, 255, 255, 255);

                    // Also set green and blue channels otherwise we get artefacts if colour box is checked
                    if (imageType == ImageType.Colour)
                    {
                        greenColors[i] = new Color32(255, 255, 255, 255);
                        blueColors[i]  = new Color32(255, 255, 255, 255);
                    }
                }
                else // use the given texture
                {
                    // select which pixel to take from the image
                    pi_x = Mathf.RoundToInt(Mathf.Lerp(0, args.srcImgWidth - 1, rowProgress));
                    pi_y = Mathf.RoundToInt(Mathf.Lerp(0, args.srcImgHeight - 1, colProgress));
                    int flatindex = (args.srcImgWidth * pi_y) + pi_x;

                    // Colour or greyscale?
                    switch (imageType)
                    {
                    case ImageType.Colour:
                        red            = (byte)Mathf.Clamp((imageColours[flatindex].r), 0, 255);
                        redColors[i]   = new Color32(255, 255, 255, red);
                        green          = (byte)Mathf.Clamp((imageColours[flatindex].g), 0, 255);
                        greenColors[i] = new Color32(255, 255, 255, green);
                        blue           = (byte)Mathf.Clamp((imageColours[flatindex].b), 0, 255);
                        blueColors[i]  = new Color32(255, 255, 255, blue);
                        break;

                    default:     // greyscale
                        f = ((float)imageColours[flatindex].r +
                             imageColours[flatindex].g +
                             imageColours[flatindex].b) / 3f;
                        red          = (byte)Mathf.Clamp((int)f, 0, 255);
                        redColors[i] = new Color32(255, 255, 255, red);
                        break;
                    }
                }
            }
            else if (args.doBlacks) // BLACK PIXELS
            {
                redColors[i] = new Color32(255, 255, 255, 0);
                if (imageType == ImageType.Colour)
                {
                    greenColors[i] = new Color32(255, 255, 255, 0);
                    blueColors[i]  = new Color32(255, 255, 255, 0);
                }
            }
        }
    }