您最多选择25个主题
主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
272 行
15 KiB
272 行
15 KiB
using System;
|
|
using UnityEngine;
|
|
using System.IO;
|
|
|
|
namespace UnityEditor.Experimental.Rendering.HDPipeline
|
|
{
|
|
public class NormalMapFilteringTexturePostprocessor : AssetPostprocessor
|
|
{
|
|
// This class will process a normal map and add the value of average normal length to the blue or alpha channel
|
|
// The texture is saved as BC7.
|
|
// Tangent space normal map: BC7 RGB (normal xy - encoded variance)
|
|
// Object space normal map: BC7 RGBA (normal xyz - encoded variance)
|
|
static string s_Suffix = "_NF";
|
|
static string s_SuffixOS = "_OSNF"; // Suffix for object space case
|
|
|
|
bool IsAssetTaggedAsTangentSpaceNormalMap()
|
|
{
|
|
return Path.GetFileNameWithoutExtension(assetPath).EndsWith(s_Suffix, StringComparison.InvariantCultureIgnoreCase);
|
|
}
|
|
|
|
bool IsAssetTaggedAsObjectSpaceNormalMap()
|
|
{
|
|
return Path.GetFileNameWithoutExtension(assetPath).EndsWith(s_SuffixOS, StringComparison.InvariantCultureIgnoreCase);
|
|
}
|
|
|
|
void OnPreprocessTexture()
|
|
{
|
|
bool isNormalMapTangentSpace = IsAssetTaggedAsTangentSpaceNormalMap();
|
|
bool isNormalMapObjectSpace = isNormalMapTangentSpace ? false : IsAssetTaggedAsObjectSpaceNormalMap();
|
|
|
|
if (isNormalMapTangentSpace || isNormalMapObjectSpace)
|
|
{
|
|
// Make sure we don't convert as a normal map.
|
|
TextureImporter textureImporter = (TextureImporter)assetImporter;
|
|
textureImporter.convertToNormalmap = false;
|
|
//textureImporter.alphaSource = isNormalMapTangentSpace ? TextureImporterAlphaSource.None : TextureImporterAlphaSource.FromInput;
|
|
//bool inputHasAlphaChannel = textureImporter.DoesSourceTextureHaveAlpha();
|
|
// Hack:
|
|
// We need Unity to create an alpha channel when using object space normal maps!
|
|
// If it doesn't find one in input, and we set TextureImporterAlphaSource.FromInput, it will ignore our alpha values we
|
|
// set with SetPixels in OnPostProcess, even if the Texture2D format is eg RGBA.
|
|
// So here, we force it to create a dummy one in all cases (from gray scale)
|
|
textureImporter.alphaSource = isNormalMapTangentSpace ? TextureImporterAlphaSource.None : TextureImporterAlphaSource.FromGrayScale;
|
|
textureImporter.alphaIsTransparency = false;
|
|
textureImporter.mipmapEnabled = true;
|
|
textureImporter.textureCompression = TextureImporterCompression.CompressedHQ; // This is BC7 for Mac/PC
|
|
//textureImporter.textureCompression = TextureImporterCompression.Uncompressed;
|
|
// We can also force a format like TextureImporterFormat.BC6H or TextureImporterFormat.BC7:
|
|
var settings = textureImporter.GetPlatformTextureSettings("Standalone");
|
|
settings.format = TextureImporterFormat.BC7;
|
|
settings.overridden = true;
|
|
textureImporter.SetPlatformTextureSettings(settings);
|
|
textureImporter.isReadable = true;
|
|
// ...works without, but Unity doc says to set it if we need read access during OnPostProcess:
|
|
// https://docs.unity3d.com/ScriptReference/AssetPostprocessor.OnPostprocessTexture.html
|
|
#pragma warning disable 618 // remove obsolete warning for this one
|
|
textureImporter.linearTexture = true; // Says deprecated but won't work without it.
|
|
#pragma warning restore 618
|
|
textureImporter.sRGBTexture = false; // But we're setting the new property just in case it changes later...
|
|
}
|
|
}
|
|
|
|
private static Color GetColor(Color[] source, int x, int y, int width, int height)
|
|
{
|
|
x = (x + width) % width; // for NPOT textures
|
|
y = (y + height) % height;
|
|
|
|
int index = y * width + x;
|
|
var c = source[index];
|
|
|
|
return c;
|
|
}
|
|
|
|
private static Vector3 GetNormal(Color[] source, int x, int y, int width, int height)
|
|
{
|
|
Vector3 n = (Vector4)GetColor(source, x, y, width, height);
|
|
n = 2.0f * n - Vector3.one;
|
|
n.Normalize();
|
|
|
|
return n;
|
|
}
|
|
|
|
private static Vector3 GetAverageNormal(Color[] source, int x, int y, int width, int height, int texelFootprintW, int texelFootprintH)
|
|
{
|
|
Vector3 averageNormal = new Vector3(0, 0, 0);
|
|
|
|
// Calculate the average color over the texel footprint.
|
|
for (int i = 0; i < texelFootprintH; ++i)
|
|
{
|
|
for (int j = 0; j < texelFootprintW; ++j)
|
|
{
|
|
averageNormal += GetNormal(source, x + j, y + i, width, height);
|
|
}
|
|
}
|
|
|
|
averageNormal /= (texelFootprintW * texelFootprintH);
|
|
|
|
return averageNormal;
|
|
}
|
|
|
|
// Converts averageNormalLength to variance and
|
|
// thresholds and remaps variance from [0, highestVarianceAllowed] to [0, 1]
|
|
private static float GetEncodedVariance(float averageNormalLength)
|
|
{
|
|
// Caution: This constant must be in sync with CommonMaterial.hlsl #define NORMALMAP_HIGHEST_VARIANCE
|
|
const float highestVarianceAllowed = 0.03125f; // 0.25 * 0.25 / 2 = 0.0625 / 2 = 0.03125;
|
|
|
|
// To decide to store or not the averageNormalLength directly we need to consider:
|
|
//
|
|
// 1) useful range vs block compression and bit encoding of that range,
|
|
// 2) the possibly nonlinear conversion to variance,
|
|
// 3) computation in the shader.
|
|
//
|
|
// For 2) we need something that can be linearly filtered by the hardware as much as possible.
|
|
// Averages of length(average normal) are obviously not equal to length( of averages of normal)
|
|
// (that's the point of normal map inferred-NDF filtering vs just using mip-mapping hardware on normal maps),
|
|
// and the formula to get to variance via the vMF lobe fit is also quite nonlinear, although not
|
|
// everywhere. We show below that the most useful part of this fit (near the 1.0 end of the length
|
|
// of the average normal) is linear and so if we would store and filter averageNormalLength anyway
|
|
// but limit our range to that part, we could just store and filter directly the variance in that
|
|
// range too. (Note though that moments are linearly filterable cf LEAN, LEADR).
|
|
// For 1), compression can further compound artifacts too so we need to consider the useful range.
|
|
//
|
|
// We recall:
|
|
//
|
|
// Ref: Frequency Domain Normal Map Filtering - http://www.cs.columbia.edu/cg/normalmap/normalmap.pdf
|
|
// (equation 21)
|
|
// The relationship between between the standard deviation of a Gaussian distribution and
|
|
// the roughness parameter of a Beckmann distribution.is roughness^2 = 2 variance
|
|
// Ref: Filtering Distributions of Normals for Shading Antialiasing, equation just after (14).
|
|
// Relationship between gaussian lobe and vMF lobe is 2 * variance = 1 / (2 * kappa) = roughness^2
|
|
// (Equation 36 of Normal map filtering based on The Order : 1886 SIGGRAPH course notes implementation).
|
|
//
|
|
// So to get variance we must use variance = 1 / (4 * kappa)
|
|
// where
|
|
// kappa = (averageNormalLength*(3.0f - averageNormalLengthSquared)) / (1.0f - averageNormalLengthSquared);
|
|
//
|
|
float averageNormalLengthSquared = averageNormalLength * averageNormalLength;
|
|
float variance = 0.0f;
|
|
if (averageNormalLengthSquared < 1.0f)
|
|
{
|
|
float kappa = (averageNormalLength * (3.0f - averageNormalLengthSquared)) / (1.0f - averageNormalLengthSquared);
|
|
variance = 1.0f / (4.0f * kappa);
|
|
}
|
|
|
|
// The variance as a function of (averageNormalLength) is quite steep near 0 length, and
|
|
// from about averageNormalLength = 0.376, variance stays under 0.2, and
|
|
// from about averageNormalLength = 0.603, variance stays under 0.1, and goes to 0 quite
|
|
// linearly as averageNormalLength goes to 1 with a slope of -1/4
|
|
// http://www.wolframalpha.com/input/?i=y(x)+:%3D+(1+-+x*x)%2F(4*(3x+-+x*x*x));+x+from+0+to+1
|
|
|
|
// Remember we do "+ min(2.0 * variance, threshold * threshold)" in NormalFiltering of CommonMaterial.hlsl
|
|
// to effectively limit the added_roughness^2
|
|
// when doing normal map filtering by modifying underlying BSDF roughness.
|
|
//
|
|
// An added variance of 0.1 gives an increase of roughness = sqrt(2 * 0.1) = 0.447, which is a huge increase already.
|
|
// An added variance of 0.03125 gives an increase of roughness = sqrt(2 * 0.03125) = 0.25, which still a lot
|
|
//
|
|
// Also remember that we use a user specified threshold to effectively limit the added_roughness^2,
|
|
// as shown above with + min(2.0 * variance, threshold * threshold)).
|
|
// We consider the relationship between the considered range of useful added variance vs that threshold:
|
|
// We have
|
|
// 2*variance_max_allowed = roughness_threshold_max_allowed^2
|
|
// variance_max_allowed = 0.5 * roughness_threshold_max_allowed^2
|
|
//
|
|
// Let's say we think an increased roughness^2 of threshold * threshold = 0.250^2 is enough such
|
|
// that we will never set the threshold in the UI to anything higher than 0.250.
|
|
// We then have:
|
|
//
|
|
// (0.250)^2 = (2 * variance_max_allowed)
|
|
// variance_max_allowed = 0.25*0.25 / 2 = 0.0625/2 = 0.03125
|
|
// 0.03125 = (1-xx)/(4*(3*x-x*x*x)) where x is lowestAverageNormalLengthAllowed
|
|
// http://www.wolframalpha.com/input/?i=0.03125+%3D++(1+-+x*x)%2F(4*(3x+-+x*x*x));+solve+for+x+from+0+to+1
|
|
// which gives our constants:
|
|
//
|
|
// highestVarianceAllowed = 0.03125f
|
|
// lowestAverageNormalLengthAllowed = 0.8695f;
|
|
//
|
|
float encodedVariance = Math.Min(variance, highestVarianceAllowed) / highestVarianceAllowed;
|
|
return encodedVariance;
|
|
}
|
|
|
|
void OnPostprocessTexture(Texture2D texture)
|
|
{
|
|
bool isNormalMapTangentSpace = IsAssetTaggedAsTangentSpaceNormalMap();
|
|
bool isNormalMapObjectSpace = isNormalMapTangentSpace ? false : IsAssetTaggedAsObjectSpaceNormalMap();
|
|
|
|
if (isNormalMapTangentSpace || isNormalMapObjectSpace)
|
|
{
|
|
// Based on The Order : 1886 SIGGRAPH course notes implementation. Sample all normal map
|
|
// texels from the base mip level that are within the footprint of the current mipmap texel.
|
|
Color[] source = texture.GetPixels(0);
|
|
for (int m = 1; m < texture.mipmapCount; m++)
|
|
{
|
|
Color[] c = texture.GetPixels(m);
|
|
|
|
int mipWidth = Math.Max(1, texture.width >> m);
|
|
int mipHeight = Math.Max(1, texture.height >> m);
|
|
int texelFootprintW = texture.width / mipWidth;
|
|
int texelFootprintH = texture.height / mipHeight;
|
|
for (int y = 0; y < mipHeight; ++y)
|
|
{
|
|
for (int x = 0; x < mipWidth; ++x)
|
|
{
|
|
Vector3 averageNormal = GetAverageNormal(source, x * texelFootprintW, y * texelFootprintH,
|
|
texture.width, texture.height, texelFootprintW, texelFootprintH);
|
|
|
|
int outputPosition = y * mipWidth + x;
|
|
|
|
// Note: As an optimization we could check what is generated in the mipmap (as it is suppose to be the average already)
|
|
// TODO: Do some test and see if it is equivalent, for now reprocess all normal from top mips.
|
|
// Vector3 existingAverageNormal = (Vector4)c[outputPosition];
|
|
// existingAverageNormal = 2.0f * existingAverageNormal - Vector3.one;
|
|
|
|
// Clamp to avoid any issue (shouldn't be required but sanitizes the normal map if needed)
|
|
// We will also write the custom data into the blue channel to streamline the unpacking
|
|
// shader code to fetch a 2 channel normal in RG whether we use normal map filtering or not.
|
|
float averageNormalLength = Math.Max(0.0f, Math.Min(1.0f, averageNormal.magnitude));
|
|
float outputValue = GetEncodedVariance(averageNormalLength);
|
|
|
|
// Finally, note that since we need to add custom data in a map channel, we can't use the Unity
|
|
// importer UI settings TextureType == NormalMap, since it leaves channel control to Unity in
|
|
// that case and will make it interpret the x,y,z input as the normal to compress (which it might
|
|
// do eg in 2 channels BC5)
|
|
//
|
|
// We need to normalize the resulting average normal and store the x,y components in the
|
|
// proper (n + 1)/2 range encoded values in the R,G channels:
|
|
averageNormal.Normalize();
|
|
c[outputPosition].r = (averageNormal.x + 1.0f) / 2.0f;
|
|
c[outputPosition].g = (averageNormal.y + 1.0f) / 2.0f;
|
|
if (isNormalMapTangentSpace)
|
|
{
|
|
c[outputPosition].b = outputValue;
|
|
c[outputPosition].a = 1.0f;
|
|
}
|
|
else
|
|
{
|
|
// Object space normal map needs 3 channels
|
|
c[outputPosition].b = (averageNormal.z + 1.0f) / 2.0f;
|
|
c[outputPosition].a = outputValue;
|
|
}
|
|
}
|
|
}
|
|
|
|
texture.SetPixels(c, m);
|
|
}
|
|
|
|
// Now overwrite the first mip average normal channel - order is important as above we read the mip0
|
|
// For mip 0, set the normal length to 1.
|
|
{
|
|
Color[] c = texture.GetPixels(0);
|
|
float outputValue = GetEncodedVariance(1.0f);
|
|
for (int i = 0; i < c.Length; i++)
|
|
{
|
|
if (isNormalMapTangentSpace)
|
|
{
|
|
c[i].b = outputValue;
|
|
c[i].a = 1.0f;
|
|
}
|
|
else
|
|
{
|
|
c[i].a = outputValue;
|
|
}
|
|
}
|
|
texture.SetPixels(c, 0);
|
|
}
|
|
// Compression will be apply after this.
|
|
texture.Apply(updateMipmaps: false, makeNoLongerReadable: true);
|
|
}
|
|
}
|
|
}
|
|
}
|