using System.Collections.Generic; using UnityEngine; using UnityEditor; [CustomEditor(typeof(ProceduralTexture2D)), CanEditMultipleObjects] public class ProceduralTexture2DEditor : Editor { ProceduralTexture2D[] targetAssets; public void OnEnable() { Object[] monoObjects = targets; targetAssets = new ProceduralTexture2D[monoObjects.Length]; for (int i = 0; i < monoObjects.Length; i++) { targetAssets[i] = monoObjects[i] as ProceduralTexture2D; } } public override void OnInspectorGUI() { serializedObject.Update(); // Input Texture EditorGUI.BeginChangeCheck(); EditorGUILayout.PropertyField(serializedObject.FindProperty("input"), new GUIContent("Texture")); if (EditorGUI.EndChangeCheck()) { serializedObject.ApplyModifiedProperties(); CopyInputTextureImportType(targetAssets[0]); } GUILayout.Space(10); // Texture Type EditorGUILayout.PropertyField(serializedObject.FindProperty("type"), new GUIContent("Texture Type")); // Include alpha for color textures if ((ProceduralTexture2D.TextureType)serializedObject.FindProperty("type").enumValueIndex == ProceduralTexture2D.TextureType.Color) EditorGUILayout.PropertyField(serializedObject.FindProperty("includeAlpha"), new GUIContent("Include Alpha")); GUILayout.Space(10); // Filtering EditorGUILayout.PropertyField(serializedObject.FindProperty("generateMipMaps"), new GUIContent("Generate Mip Maps")); EditorGUILayout.PropertyField(serializedObject.FindProperty("filterMode"), new GUIContent("Filter mode")); Rect sliderRect = GUILayoutUtility.GetLastRect(); if (serializedObject.FindProperty("generateMipMaps").boolValue == true) { sliderRect = new Rect(sliderRect.x, sliderRect.y + sliderRect.height, sliderRect.width, sliderRect.height); PropertyIntSlider(sliderRect, serializedObject.FindProperty("anisoLevel"), 0, 16, new GUIContent("Aniso Level")); GUILayoutUtility.GetRect(new GUIContent("Aniso Level"), EditorStyles.label); } GUILayout.Space(10); // Compression EditorGUILayout.PropertyField(serializedObject.FindProperty("compressionQuality"), new GUIContent("Compression")); // Memory size display string size = targetAssets.Length == 1 && targetAssets[0].memoryUsageBytes > 0 ? SizeSuffix(targetAssets[0].memoryUsageBytes) : "--"; EditorGUILayout.LabelField("Size in memory: " + size, EditorStyles.centeredGreyMiniLabel); GUILayout.Space(10); // Apply changes button GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); if (GUILayout.Button("Apply")) for (int i = 0; i < targetAssets.Length; i++) PreprocessData(targetAssets[i]); GUILayout.EndHorizontal(); // Normal compression warning if (targetAssets[0].type == ProceduralTexture2D.TextureType.Normal && targetAssets[0].compressionQuality != ProceduralTexture2D.CompressionLevel.HighQuality) EditorGUILayout.HelpBox("High quality compression recommended for normal maps", MessageType.Info); // Unapplied changes warning if (UnappliedSettingChanges(targetAssets[0]) == true) EditorGUILayout.HelpBox("Unapplied settings", MessageType.Info); serializedObject.ApplyModifiedProperties(); } // A slider function that takes a SerializedProperty void PropertyIntSlider(Rect position, SerializedProperty property, int leftValue, int rightValue, GUIContent label) { label = EditorGUI.BeginProperty(position, label, property); EditorGUI.BeginChangeCheck(); var newValue = EditorGUI.IntSlider(position, label, property.intValue, leftValue, rightValue); if (EditorGUI.EndChangeCheck()) property.intValue = newValue; EditorGUI.EndProperty(); } private readonly string[] SizeSuffixes = { "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" }; private string SizeSuffix(long value, int decimalPlaces = 1) { if (value < 0) { return "-" + SizeSuffix(-value); } if (value == 0) { return string.Format("{0:n" + decimalPlaces + "} bytes", 0); } // mag is 0 for bytes, 1 for KB, 2, for MB, etc. int mag = (int)Mathf.Log(value, 1024); // 1L << (mag * 10) == 2 ^ (10 * mag) // [i.e. the number of bytes in the unit corresponding to mag] decimal adjustedSize = (decimal)value / (1L << (mag * 10)); // make adjustment when the value is large enough that // it would round up to 1000 or more if (System.Math.Round(adjustedSize, decimalPlaces) >= 1000) { mag += 1; adjustedSize /= 1024; } return string.Format("{0:n" + decimalPlaces + "} {1}", adjustedSize, SizeSuffixes[mag]); } private void CopyInputTextureImportType(ProceduralTexture2D target) { string path = AssetDatabase.GetAssetPath(target.input); TextureImporter inputImporter = (TextureImporter)TextureImporter.GetAtPath(path); switch (inputImporter.textureType) { case TextureImporterType.NormalMap: target.type = ProceduralTexture2D.TextureType.Normal; break; default: target.type = ProceduralTexture2D.TextureType.Color; break; } } private bool UnappliedSettingChanges(ProceduralTexture2D target) { if(target.currentInput != target.input || target.currentIncludeAlpha != target.includeAlpha || target.currentGenerateMipMaps != target.generateMipMaps || target.currentFilterMode != target.filterMode || target.currentAnisoLevel != target.anisoLevel || target.currentCompressionQuality != target.compressionQuality) { return true; } else { return false; } } /*********************************************************************/ /*********************************************************************/ /*************Procedural Stochastic Texturing Pre-process*************/ /*********************************************************************/ /*********************************************************************/ const float GAUSSIAN_AVERAGE = 0.5f; // Expectation of the Gaussian distribution const float GAUSSIAN_STD = 0.1666f; // Std of the Gaussian distribution const int LUT_WIDTH = 128; // Size of the look-up table private static int stepCounter = 0; private static int totalSteps = 0; struct TextureData { public Color[] data; public int width; public int height; public TextureData(int w, int h) { width = w; height = h; data = new Color[w * h]; } public TextureData(TextureData td) { width = td.width; height = td.height; data = new Color[width * height]; for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) data[y * width + x] = td.data[y * width + x]; } public Color GetColor(int w, int h) { return data[h * width + w]; } public ref Color GetColorRef(int w, int h) { return ref data[h * width + w]; } public void SetColorAt(int w, int h, Color value) { data[h * width + w] = value; } }; private static void PreprocessData(ProceduralTexture2D target) { if (target.input == null) return; // Init progress bar stepCounter = 0; totalSteps = (target.type != ProceduralTexture2D.TextureType.Other ? 4 : 0) + (target.type != ProceduralTexture2D.TextureType.Other ? 9 : 12) + 1; EditorUtility.DisplayProgressBar("Pre-processing Procedural Texture Data", target.name, (float)stepCounter / (totalSteps - 1)); // Section 1.4 Improvement: using a decorrelated color space for Color RGB and Normal XYZ textures TextureFormat inputFormat = TextureFormat.RGB24; TextureData albedoData = TextureToTextureData(target.input, ref inputFormat); TextureData decorrelated = new TextureData(albedoData); if (target.type != ProceduralTexture2D.TextureType.Other) DecorrelateColorSpace(ref albedoData, ref decorrelated, ref target.colorSpaceVector1, ref target.colorSpaceVector2, ref target.colorSpaceVector3, ref target.colorSpaceOrigin, target.name); ComputeCompressionScalers(target); // Perform precomputations TextureData Tinput = new TextureData(decorrelated.width, decorrelated.height); TextureData invT = new TextureData(LUT_WIDTH, (int)(Mathf.Log((float)Tinput.width) / Mathf.Log(2.0f))); // Height = Number of prefiltered LUT levels List channelsToProcess = new List { 0, 1, 2 }; if ((target.type == ProceduralTexture2D.TextureType.Color && target.includeAlpha == true) || target.type == ProceduralTexture2D.TextureType.Other) channelsToProcess.Add(3); Precomputations(ref decorrelated, channelsToProcess, ref Tinput, ref invT, target.name); RescaleForCompression(target, ref Tinput); EditorUtility.DisplayProgressBar("Pre-processing Procedural Texture Data", target.name, (float)stepCounter++ / (totalSteps - 1)); // Serialize precomputed data and setup material FinalizePrecomputedTextures(ref inputFormat, target, ref Tinput, ref invT); target.memoryUsageBytes = target.Tinput.GetRawTextureData().Length + target.invT.GetRawTextureData().Length; EditorUtility.ClearProgressBar(); // Update current applied settings target.currentInput = target.input; target.currentIncludeAlpha = target.includeAlpha; target.currentGenerateMipMaps = target.generateMipMaps; target.currentFilterMode = target.filterMode; target.currentAnisoLevel = target.anisoLevel; target.currentCompressionQuality = target.compressionQuality; } static TextureData TextureToTextureData(Texture2D input, ref TextureFormat inputFormat) { // Modify input texture import settings temporarily string texpath = AssetDatabase.GetAssetPath(input); TextureImporter importer = (TextureImporter)TextureImporter.GetAtPath(texpath); TextureImporterCompression prev = importer.textureCompression; TextureImporterType prevType = importer.textureType; bool linearInput = importer.sRGBTexture == false || importer.textureType == TextureImporterType.NormalMap; bool prevReadable = importer.isReadable; if (importer != null) { importer.textureType = TextureImporterType.Default; importer.isReadable = true; importer.textureCompression = TextureImporterCompression.Uncompressed; AssetDatabase.ImportAsset(texpath, ImportAssetOptions.ForceUpdate); inputFormat = input.format; } // Copy input texture pixel data Color[] colors = input.GetPixels(); TextureData res = new TextureData(input.width, input.height); for (int x = 0; x < res.width; x++) { for (int y = 0; y < res.height; y++) { res.SetColorAt(x, y, linearInput || PlayerSettings.colorSpace == ColorSpace.Gamma ? colors[y * res.width + x] : colors[y * res.width + x].linear); } } // Revert input texture settings if (importer != null) { importer.textureType = prevType; importer.isReadable = prevReadable; importer.textureCompression = prev; AssetDatabase.ImportAsset(texpath, ImportAssetOptions.ForceUpdate); } return res; } static void FinalizePrecomputedTextures(ref TextureFormat inputFormat, ProceduralTexture2D target, ref TextureData Tinput, ref TextureData invT) { // Serialize precomputed data as new subasset texture. Reuse existing texture if possible to avoid breaking texture references in shadergraph. if(target.Tinput == null) { target.Tinput = new Texture2D(Tinput.width, Tinput.height, inputFormat, target.generateMipMaps, true); AssetDatabase.AddObjectToAsset(target.Tinput, target); } target.Tinput.Resize(Tinput.width, Tinput.height, inputFormat, target.generateMipMaps); target.Tinput.name = target.input.name + "_T"; target.Tinput.SetPixels(Tinput.data); target.Tinput.wrapMode = TextureWrapMode.Repeat; target.Tinput.filterMode = target.filterMode; target.Tinput.anisoLevel = target.anisoLevel; target.Tinput.Apply(); if (target.compressionQuality != ProceduralTexture2D.CompressionLevel.None) { if(target.compressionQuality == ProceduralTexture2D.CompressionLevel.HighQuality) EditorUtility.CompressTexture(target.Tinput, TextureFormat.BC7, (int)target.compressionQuality); else if (inputFormat == TextureFormat.RGBA32) EditorUtility.CompressTexture(target.Tinput, TextureFormat.DXT5, (int)target.compressionQuality); else EditorUtility.CompressTexture(target.Tinput, TextureFormat.DXT1, (int)target.compressionQuality); } target.Tinput.Apply(); if (target.invT == null) { target.invT = new Texture2D(invT.width, invT.height, inputFormat, false, true); AssetDatabase.AddObjectToAsset(target.invT, target); } target.invT.Resize(invT.width, invT.height, inputFormat, false); target.invT.name = target.input.name + "_invT"; target.invT.wrapMode = TextureWrapMode.Clamp; target.invT.filterMode = FilterMode.Bilinear; target.invT.SetPixels(invT.data); target.invT.Apply(); // Update asset database AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } private static void Precomputations( ref TextureData input, // input: example image List channels, // input: channels to process ref TextureData Tinput, // output: T(input) image ref TextureData invT, // output: T^{-1} look-up table string assetName) { // Section 1.3.2 Applying the histogram transformation T on the input foreach (int channel in channels) { ComputeTinput(ref input, ref Tinput, channel); EditorUtility.DisplayProgressBar("Pre-processing Procedural Texture Data", assetName, (float)stepCounter++ / (totalSteps - 1)); } // Section 1.3.3 Precomputing the inverse histogram transformation T^{-1} foreach (int channel in channels) { ComputeinvT(ref input, ref invT, channel); EditorUtility.DisplayProgressBar("Pre-processing Procedural Texture Data", assetName, (float)stepCounter++ / (totalSteps - 1)); } // Section 1.5 Improvement: prefiltering the look-up table foreach (int channel in channels) { PrefilterLUT(ref Tinput, ref invT, channel); EditorUtility.DisplayProgressBar("Pre-processing Procedural Texture Data", assetName, (float)stepCounter++ / (totalSteps - 1)); } } private static void ComputeCompressionScalers(ProceduralTexture2D target) { target.compressionScalers = Vector4.one; if (target.compressionQuality != ProceduralTexture2D.CompressionLevel.None && target.type != ProceduralTexture2D.TextureType.Other) { target.compressionScalers.x = 1.0f / target.colorSpaceVector1.magnitude; target.compressionScalers.y = 1.0f / target.colorSpaceVector2.magnitude; target.compressionScalers.z = 1.0f / target.colorSpaceVector3.magnitude; } } private static void RescaleForCompression(ProceduralTexture2D target, ref TextureData Tinput) { int channelCount = (target.type == ProceduralTexture2D.TextureType.Color && target.includeAlpha == true) || target.type == ProceduralTexture2D.TextureType.Other ? 4 : 3; // If we use DXT compression // we need to rescale the Gaussian channels (see Section 1.6) if (target.compressionQuality != ProceduralTexture2D.CompressionLevel.None && target.type != ProceduralTexture2D.TextureType.Other) { for (int y = 0; y < Tinput.height; y++) for (int x = 0; x < Tinput.width; x++) for (int i = 0; i < channelCount; i++) { float v = Tinput.GetColor(x, y)[i]; v = (v - 0.5f) / target.compressionScalers[i] + 0.5f; Tinput.GetColorRef(x, y)[i] = v; } } } /*****************************************************************************/ /**************** Section 1.3.1 Target Gaussian distribution *****************/ /*****************************************************************************/ private static float Erf(float x) { // Save the sign of x int sign = 1; if (x < 0) sign = -1; x = Mathf.Abs(x); // A&S formula 7.1.26 float t = 1.0f / (1.0f + 0.3275911f * x); float y = 1.0f - (((((1.061405429f * t + -1.453152027f) * t) + 1.421413741f) * t + -0.284496736f) * t + 0.254829592f) * t * Mathf.Exp(-x * x); return sign * y; } private static float ErfInv(float x) { float w, p; w = -Mathf.Log((1.0f - x) * (1.0f + x)); if (w < 5.000000f) { w = w - 2.500000f; p = 2.81022636e-08f; p = 3.43273939e-07f + p * w; p = -3.5233877e-06f + p * w; p = -4.39150654e-06f + p * w; p = 0.00021858087f + p * w; p = -0.00125372503f + p * w; p = -0.00417768164f + p * w; p = 0.246640727f + p * w; p = 1.50140941f + p * w; } else { w = Mathf.Sqrt(w) - 3.000000f; p = -0.000200214257f; p = 0.000100950558f + p * w; p = 0.00134934322f + p * w; p = -0.00367342844f + p * w; p = 0.00573950773f + p * w; p = -0.0076224613f + p * w; p = 0.00943887047f + p * w; p = 1.00167406f + p * w; p = 2.83297682f + p * w; } return p * x; } private static float CDF(float x, float mu, float sigma) { float U = 0.5f * (1 + Erf((x - mu) / (sigma * Mathf.Sqrt(2.0f)))); return U; } private static float invCDF(float U, float mu, float sigma) { float x = sigma * Mathf.Sqrt(2.0f) * ErfInv(2.0f * U - 1.0f) + mu; return x; } /*****************************************************************************/ /**** Section 1.3.2 Applying the histogram transformation T on the input *****/ /*****************************************************************************/ private struct PixelSortStruct { public int x; public int y; public float value; }; private static void ComputeTinput(ref TextureData input, ref TextureData T_input, int channel) { // Sort pixels of example image PixelSortStruct[] sortedInputValues = new PixelSortStruct[input.width * input.height]; for (int y = 0; y < input.height; y++) for (int x = 0; x < input.width; x++) { sortedInputValues[y * input.width + x].x = x; sortedInputValues[y * input.width + x].y = y; sortedInputValues[y * input.width + x].value = input.GetColor(x, y)[channel]; } System.Array.Sort(sortedInputValues, (x, y) => x.value.CompareTo(y.value)); // Assign Gaussian value to each pixel for (uint i = 0; i < sortedInputValues.Length; i++) { // Pixel coordinates int x = sortedInputValues[i].x; int y = sortedInputValues[i].y; // Input quantile (given by its order in the sorting) float U = (i + 0.5f) / (sortedInputValues.Length); // Gaussian quantile float G = invCDF(U, GAUSSIAN_AVERAGE, GAUSSIAN_STD); // Store T_input.GetColorRef(x, y)[channel] = G; } } /*****************************************************************************/ /* Section 1.3.3 Precomputing the inverse histogram transformation T^{-1} */ /*****************************************************************************/ private static void ComputeinvT(ref TextureData input, ref TextureData Tinv, int channel) { // Sort pixels of example image float[] sortedInputValues = new float[input.width * input.height]; for (int y = 0; y < input.height; y++) for (int x = 0; x < input.width; x++) { sortedInputValues[y * input.width + x] = input.GetColor(x, y)[channel]; } System.Array.Sort(sortedInputValues); // Generate Tinv look-up table for (int i = 0; i < Tinv.width; i++) { // Gaussian value in [0, 1] float G = (i + 0.5f) / (Tinv.width); // Quantile value float U = CDF(G, GAUSSIAN_AVERAGE, GAUSSIAN_STD); // Find quantile in sorted pixel values int index = (int)Mathf.Floor(U * sortedInputValues.Length); // Get input value float I = sortedInputValues[index]; // Store in LUT Tinv.GetColorRef(i, 0)[channel] = I; } } /*****************************************************************************/ /******** Section 1.4 Improvement: using a decorrelated color space **********/ /*****************************************************************************/ // Compute the eigen vectors of the histogram of the input private static void ComputeEigenVectors(ref TextureData input, Vector3[] eigenVectors) { // First and second order moments float R = 0, G = 0, B = 0, RR = 0, GG = 0, BB = 0, RG = 0, RB = 0, GB = 0; for (int y = 0; y < input.height; y++) { for (int x = 0; x < input.width; x++) { Color col = input.GetColor(x, y); R += col.r; G += col.g; B += col.b; RR += col.r * col.r; GG += col.g * col.g; BB += col.b * col.b; RG += col.r * col.g; RB += col.r * col.b; GB += col.g * col.b; } } R /= (float)(input.width * input.height); G /= (float)(input.width * input.height); B /= (float)(input.width * input.height); RR /= (float)(input.width * input.height); GG /= (float)(input.width * input.height); BB /= (float)(input.width * input.height); RG /= (float)(input.width * input.height); RB /= (float)(input.width * input.height); GB /= (float)(input.width * input.height); // Covariance matrix double[][] covarMat = new double[3][]; for (int i = 0; i < 3; i++) covarMat[i] = new double[3]; covarMat[0][0] = RR - R * R; covarMat[0][1] = RG - R * G; covarMat[0][2] = RB - R * B; covarMat[1][0] = RG - R * G; covarMat[1][1] = GG - G * G; covarMat[1][2] = GB - G * B; covarMat[2][0] = RB - R * B; covarMat[2][1] = GB - G * B; covarMat[2][2] = BB - B * B; // Find eigen values and vectors using Jacobi algorithm double[][] eigenVectorsTemp = new double[3][]; for (int i = 0; i < 3; i++) eigenVectorsTemp[i] = new double[3]; double[] eigenValuesTemp = new double[3]; ComputeEigenValuesAndVectors(covarMat, eigenVectorsTemp, eigenValuesTemp); // Set return values eigenVectors[0] = new Vector3((float)eigenVectorsTemp[0][0], (float)eigenVectorsTemp[1][0], (float)eigenVectorsTemp[2][0]); eigenVectors[1] = new Vector3((float)eigenVectorsTemp[0][1], (float)eigenVectorsTemp[1][1], (float)eigenVectorsTemp[2][1]); eigenVectors[2] = new Vector3((float)eigenVectorsTemp[0][2], (float)eigenVectorsTemp[1][2], (float)eigenVectorsTemp[2][2]); } // ---------------------------------------------------------------------------- // Numerical diagonalization of 3x3 matrcies // Copyright (C) 2006 Joachim Kopp // ---------------------------------------------------------------------------- // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA // ---------------------------------------------------------------------------- // Calculates the eigenvalues and normalized eigenvectors of a symmetric 3x3 // matrix A using the Jacobi algorithm. // The upper triangular part of A is destroyed during the calculation, // the diagonal elements are read but not destroyed, and the lower // triangular elements are not referenced at all. // ---------------------------------------------------------------------------- // Parameters: // A: The symmetric input matrix // Q: Storage buffer for eigenvectors // w: Storage buffer for eigenvalues // ---------------------------------------------------------------------------- // Return value: // 0: Success // -1: Error (no convergence) private static int ComputeEigenValuesAndVectors(double[][] A, double[][] Q, double[] w) { const int n = 3; double sd, so; // Sums of diagonal resp. off-diagonal elements double s, c, t; // sin(phi), cos(phi), tan(phi) and temporary storage double g, h, z, theta; // More temporary storage double thresh; // Initialize Q to the identitity matrix for (int i = 0; i < n; i++) { Q[i][i] = 1.0; for (int j = 0; j < i; j++) Q[i][j] = Q[j][i] = 0.0; } // Initialize w to diag(A) for (int i = 0; i < n; i++) w[i] = A[i][i]; // Calculate SQR(tr(A)) sd = 0.0; for (int i = 0; i < n; i++) sd += System.Math.Abs(w[i]); sd = sd * sd; // Main iteration loop for (int nIter = 0; nIter < 50; nIter++) { // Test for convergence so = 0.0; for (int p = 0; p < n; p++) for (int q = p + 1; q < n; q++) so += System.Math.Abs(A[p][q]); if (so == 0.0) return 0; if (nIter < 4) thresh = 0.2 * so / (n * n); else thresh = 0.0; // Do sweep for (int p = 0; p < n; p++) { for (int q = p + 1; q < n; q++) { g = 100.0 * System.Math.Abs(A[p][q]); if (nIter > 4 && System.Math.Abs(w[p]) + g == System.Math.Abs(w[p]) && System.Math.Abs(w[q]) + g == System.Math.Abs(w[q])) { A[p][q] = 0.0; } else if (System.Math.Abs(A[p][q]) > thresh) { // Calculate Jacobi transformation h = w[q] - w[p]; if (System.Math.Abs(h) + g == System.Math.Abs(h)) { t = A[p][q] / h; } else { theta = 0.5 * h / A[p][q]; if (theta < 0.0) t = -1.0 / (System.Math.Sqrt(1.0 + (theta * theta)) - theta); else t = 1.0 / (System.Math.Sqrt(1.0 + (theta * theta)) + theta); } c = 1.0 / System.Math.Sqrt(1.0 + (t * t)); s = t * c; z = t * A[p][q]; // Apply Jacobi transformation A[p][q] = 0.0; w[p] -= z; w[q] += z; for (int r = 0; r < p; r++) { t = A[r][p]; A[r][p] = c * t - s * A[r][q]; A[r][q] = s * t + c * A[r][q]; } for (int r = p + 1; r < q; r++) { t = A[p][r]; A[p][r] = c * t - s * A[r][q]; A[r][q] = s * t + c * A[r][q]; } for (int r = q + 1; r < n; r++) { t = A[p][r]; A[p][r] = c * t - s * A[q][r]; A[q][r] = s * t + c * A[q][r]; } // Update eigenvectors for (int r = 0; r < n; r++) { t = Q[r][p]; Q[r][p] = c * t - s * Q[r][q]; Q[r][q] = s * t + c * Q[r][q]; } } } } } return -1; } // Main function of Section 1.4 private static void DecorrelateColorSpace( ref TextureData input, // input: example image ref TextureData input_decorrelated, // output: decorrelated input ref Vector3 colorSpaceVector1, // output: color space vector1 ref Vector3 colorSpaceVector2, // output: color space vector2 ref Vector3 colorSpaceVector3, // output: color space vector3 ref Vector3 colorSpaceOrigin, // output: color space origin string assetName) { // Compute the eigenvectors of the histogram Vector3[] eigenvectors = new Vector3[3]; ComputeEigenVectors(ref input, eigenvectors); EditorUtility.DisplayProgressBar("Pre-processing Procedural Texture Data", assetName, (float)stepCounter++ / (totalSteps - 1)); // Rotate to eigenvector space for (int y = 0; y < input.height; y++) for (int x = 0; x < input.width; x++) for (int channel = 0; channel < 3; ++channel) { // Get current color Color color = input.GetColor(x, y); Vector3 vec = new Vector3(color.r, color.g, color.b); // Project on eigenvector float new_channel_value = Vector3.Dot(vec, eigenvectors[channel]); // Store input_decorrelated.GetColorRef(x, y)[channel] = new_channel_value; } EditorUtility.DisplayProgressBar("Pre-processing Procedural Texture Data", assetName, (float)stepCounter++ / (totalSteps - 1)); // Compute ranges of the new color space Vector2[] colorSpaceRanges = new Vector2[3]{ new Vector2(float.MaxValue, float.MinValue), new Vector2(float.MaxValue, float.MinValue), new Vector2(float.MaxValue, float.MinValue) }; for (int y = 0; y < input.height; y++) for (int x = 0; x < input.width; x++) for (int channel = 0; channel < 3; ++channel) { colorSpaceRanges[channel].x = Mathf.Min(colorSpaceRanges[channel].x, input_decorrelated.GetColor(x, y)[channel]); colorSpaceRanges[channel].y = Mathf.Max(colorSpaceRanges[channel].y, input_decorrelated.GetColor(x, y)[channel]); } EditorUtility.DisplayProgressBar("Pre-processing Procedural Texture Data", assetName, (float)stepCounter++ / (totalSteps - 1)); // Remap range to [0, 1] for (int y = 0; y < input.height; y++) for (int x = 0; x < input.width; x++) for (int channel = 0; channel < 3; ++channel) { // Get current value float value = input_decorrelated.GetColor(x, y)[channel]; // Remap in [0, 1] float remapped_value = (value - colorSpaceRanges[channel].x) / (colorSpaceRanges[channel].y - colorSpaceRanges[channel].x); // Store input_decorrelated.GetColorRef(x, y)[channel] = remapped_value; } EditorUtility.DisplayProgressBar("Pre-processing Procedural Texture Data", assetName, (float)stepCounter++ / (totalSteps - 1)); // Compute color space origin and vectors scaled for the normalized range colorSpaceOrigin.x = colorSpaceRanges[0].x * eigenvectors[0].x + colorSpaceRanges[1].x * eigenvectors[1].x + colorSpaceRanges[2].x * eigenvectors[2].x; colorSpaceOrigin.y = colorSpaceRanges[0].x * eigenvectors[0].y + colorSpaceRanges[1].x * eigenvectors[1].y + colorSpaceRanges[2].x * eigenvectors[2].y; colorSpaceOrigin.z = colorSpaceRanges[0].x * eigenvectors[0].z + colorSpaceRanges[1].x * eigenvectors[1].z + colorSpaceRanges[2].x * eigenvectors[2].z; colorSpaceVector1.x = eigenvectors[0].x * (colorSpaceRanges[0].y - colorSpaceRanges[0].x); colorSpaceVector1.y = eigenvectors[0].y * (colorSpaceRanges[0].y - colorSpaceRanges[0].x); colorSpaceVector1.z = eigenvectors[0].z * (colorSpaceRanges[0].y - colorSpaceRanges[0].x); colorSpaceVector2.x = eigenvectors[1].x * (colorSpaceRanges[1].y - colorSpaceRanges[1].x); colorSpaceVector2.y = eigenvectors[1].y * (colorSpaceRanges[1].y - colorSpaceRanges[1].x); colorSpaceVector2.z = eigenvectors[1].z * (colorSpaceRanges[1].y - colorSpaceRanges[1].x); colorSpaceVector3.x = eigenvectors[2].x * (colorSpaceRanges[2].y - colorSpaceRanges[2].x); colorSpaceVector3.y = eigenvectors[2].y * (colorSpaceRanges[2].y - colorSpaceRanges[2].x); colorSpaceVector3.z = eigenvectors[2].z * (colorSpaceRanges[2].y - colorSpaceRanges[2].x); } /*****************************************************************************/ /* ===== Section 1.5 Improvement: prefiltering the look-up table =========== */ /*****************************************************************************/ // Compute average subpixel variance at a given LOD private static float ComputeLODAverageSubpixelVariance(ref TextureData image, int LOD, int channel) { // Window width associated with int windowWidth = 1 << LOD; // Compute average variance in all the windows float average_window_variance = 0.0f; // Loop over al the windows for (int window_y = 0; window_y < image.height; window_y += windowWidth) for (int window_x = 0; window_x < image.width; window_x += windowWidth) { // Estimate variance of current window float v = 0.0f; float v2 = 0.0f; for (int y = 0; y < windowWidth; y++) for (int x = 0; x < windowWidth; x++) { float value = image.GetColor(window_x + x, window_y + y)[channel]; v += value; v2 += value * value; } v /= (float)(windowWidth * windowWidth); v2 /= (float)(windowWidth * windowWidth); float window_variance = Mathf.Max(0.0f, v2 - v * v); // Update average average_window_variance += window_variance / (image.width * image.height / windowWidth / windowWidth); } return average_window_variance; } // Filter LUT by sampling a Gaussian N(mu, std²) private static float FilterLUTValueAtx(ref TextureData LUT, float x, float std, int channel) { // Number of samples for filtering (heuristic: twice the LUT resolution) const int numberOfSamples = 2 * LUT_WIDTH; // Filter float filtered_value = 0.0f; for (int sample = 0; sample < numberOfSamples; sample++) { // Quantile used to sample the Gaussian float U = (sample + 0.5f) / numberOfSamples; // Sample the Gaussian float sample_x = invCDF(U, x, std); // Find sample texel in LUT (the LUT covers the domain [0, 1]) int sample_texel = Mathf.Max(0, Mathf.Min(LUT_WIDTH - 1, (int)Mathf.Floor(sample_x * LUT_WIDTH))); // Fetch LUT at level 0 float sample_value = LUT.GetColor(sample_texel, 0)[channel]; // Accumulate filtered_value += sample_value; } // Normalize and return filtered_value /= (float)numberOfSamples; return filtered_value; } // Main function of section 1.5 private static void PrefilterLUT(ref TextureData image_T_Input, ref TextureData LUT_Tinv, int channel) { // Prefilter for (int LOD = 1; LOD < LUT_Tinv.height; LOD++) { // Compute subpixel variance at LOD float window_variance = ComputeLODAverageSubpixelVariance(ref image_T_Input, LOD, channel); float window_std = Mathf.Sqrt(window_variance); // Prefilter LUT with Gaussian kernel of this variance for (int i = 0; i < LUT_Tinv.width; i++) { // Texel position in [0, 1] float x_texel = (i + 0.5f) / LUT_Tinv.width; // Filter look-up table around this position with Gaussian kernel float filteredValue = FilterLUTValueAtx(ref LUT_Tinv, x_texel, window_std, channel); // Store filtered value LUT_Tinv.GetColorRef(i, LOD)[channel] = filteredValue; } } } }