您最多选择25个主题 主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 

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);
}
}
}
}