469 lines
18 KiB
C#
469 lines
18 KiB
C#
using UnityEngine;
|
|
using System;
|
|
using UnityEngine.UIElements;
|
|
using UnityEditor.UIElements;
|
|
using UnityEngine.Experimental.Rendering;
|
|
|
|
namespace UnityEditor.Rendering.LookDev
|
|
{
|
|
/// <summary>
|
|
/// Lighting environment used in LookDev
|
|
/// </summary>
|
|
public class Environment : ScriptableObject
|
|
{
|
|
[SerializeField]
|
|
string m_CubemapGUID;
|
|
Cubemap m_Cubemap;
|
|
|
|
internal bool isCubemapOnly { get; private set; } = false;
|
|
|
|
/// <summary>
|
|
/// Offset on the longitude. Affect both sky and sun position in Shadow part
|
|
/// </summary>
|
|
public float rotation = 0.0f;
|
|
/// <summary>
|
|
/// Exposure to use with this Sky
|
|
/// </summary>
|
|
public float exposure = 0f;
|
|
|
|
// Setup default position to be on the sun in the default HDRI.
|
|
// This is important as the defaultHDRI don't call the set brightest spot function on first call.
|
|
[SerializeField]
|
|
float m_Latitude = 60.0f; // [-90..90]
|
|
[SerializeField]
|
|
float m_Longitude = 299.0f; // [0..360[
|
|
|
|
/// <summary>
|
|
/// The shading tint to used when computing shadow from sun
|
|
/// </summary>
|
|
public Color shadowColor = new Color(0.7f, 0.7f, 0.7f);
|
|
|
|
/// <summary>
|
|
/// The cubemap used for this part of the lighting environment
|
|
/// </summary>
|
|
public Cubemap cubemap
|
|
{
|
|
get
|
|
{
|
|
if (m_Cubemap == null || m_Cubemap.Equals(null))
|
|
LoadCubemap();
|
|
return m_Cubemap;
|
|
}
|
|
set
|
|
{
|
|
m_Cubemap = value;
|
|
m_CubemapGUID = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(m_Cubemap));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The Latitude position of the sun casting shadows
|
|
/// </summary>
|
|
public float sunLatitude
|
|
{
|
|
get => m_Latitude;
|
|
set => m_Latitude = ClampLatitude(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The Longitude position of the sun casting shadows
|
|
/// </summary>
|
|
public float sunLongitude
|
|
{
|
|
get => m_Longitude;
|
|
set => m_Longitude = ClampLongitude(value);
|
|
}
|
|
|
|
internal static float ClampLatitude(float value) => Mathf.Clamp(value, -90, 90);
|
|
|
|
internal static float ClampLongitude(float value)
|
|
{
|
|
value = value % 360f;
|
|
if (value < 0.0)
|
|
value += 360f;
|
|
return value;
|
|
}
|
|
|
|
internal void UpdateSunPosition(Light sun)
|
|
=> sun.transform.rotation = Quaternion.Euler(sunLatitude, rotation + sunLongitude, 0f);
|
|
|
|
/// <summary>
|
|
/// Compute sun position to be brightest spot of the sky
|
|
/// </summary>
|
|
public void ResetToBrightestSpot()
|
|
=> EnvironmentElement.ResetToBrightestSpot(this);
|
|
|
|
void LoadCubemap()
|
|
{
|
|
m_Cubemap = null;
|
|
|
|
GUID storedGUID;
|
|
GUID.TryParse(m_CubemapGUID, out storedGUID);
|
|
if (!storedGUID.Empty())
|
|
{
|
|
string path = AssetDatabase.GUIDToAssetPath(m_CubemapGUID);
|
|
m_Cubemap = AssetDatabase.LoadAssetAtPath<Cubemap>(path);
|
|
}
|
|
}
|
|
|
|
internal void CopyTo(Environment other)
|
|
{
|
|
other.cubemap = cubemap;
|
|
other.exposure = exposure;
|
|
other.rotation = rotation;
|
|
other.sunLatitude = sunLatitude;
|
|
other.sunLongitude = sunLongitude;
|
|
other.shadowColor = shadowColor;
|
|
other.name = name + " (copy)";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Implicit conversion operator to runtime version of sky datas
|
|
/// </summary>
|
|
/// <param name="sky">Editor version of the datas</param>
|
|
public UnityEngine.Rendering.LookDev.Sky sky
|
|
=> new UnityEngine.Rendering.LookDev.Sky()
|
|
{
|
|
cubemap = cubemap,
|
|
longitudeOffset = rotation,
|
|
exposure = exposure
|
|
};
|
|
|
|
internal static Environment GetTemporaryEnvironmentForCubemap(Cubemap cubemap)
|
|
{
|
|
Environment result = ScriptableObject.CreateInstance<Environment>();
|
|
result.cubemap = cubemap;
|
|
result.isCubemapOnly = true;
|
|
return result;
|
|
}
|
|
|
|
internal bool HasCubemapAssetChanged(Cubemap cubemap)
|
|
{
|
|
if (cubemap == null)
|
|
return !String.IsNullOrEmpty(m_CubemapGUID);
|
|
|
|
return m_CubemapGUID != AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(cubemap));
|
|
}
|
|
}
|
|
|
|
[CustomEditor(typeof(Environment))]
|
|
class EnvironmentEditor : Editor
|
|
{
|
|
//display nothing
|
|
public sealed override VisualElement CreateInspectorGUI() => null;
|
|
|
|
// Don't use ImGUI
|
|
public sealed override void OnInspectorGUI() { }
|
|
|
|
//but make preview in Project window
|
|
override public Texture2D RenderStaticPreview(string assetPath, UnityEngine.Object[] subAssets, int width, int height)
|
|
=> EnvironmentElement.GetLatLongThumbnailTexture(target as Environment, width);
|
|
}
|
|
|
|
interface IBendable<T>
|
|
{
|
|
void Bind(T data);
|
|
}
|
|
|
|
class EnvironmentElement : VisualElement, IBendable<Environment>
|
|
{
|
|
internal const int k_SkyThumbnailWidth = 200;
|
|
internal const int k_SkyThumbnailHeight = 100;
|
|
static Material s_cubeToLatlongMaterial;
|
|
static Material cubeToLatlongMaterial
|
|
{
|
|
get
|
|
{
|
|
if (s_cubeToLatlongMaterial == null || s_cubeToLatlongMaterial.Equals(null))
|
|
{
|
|
s_cubeToLatlongMaterial = new Material(Shader.Find("Hidden/LookDev/CubeToLatlong"));
|
|
}
|
|
return s_cubeToLatlongMaterial;
|
|
}
|
|
}
|
|
|
|
VisualElement environmentParams;
|
|
Environment environment;
|
|
|
|
Image latlong;
|
|
ObjectField skyCubemapField;
|
|
FloatField skyRotationOffset;
|
|
FloatField skyExposureField;
|
|
Vector2Field sunPosition;
|
|
ColorField shadowColor;
|
|
TextField environmentName;
|
|
|
|
Action OnChangeCallback;
|
|
|
|
public Environment target => environment;
|
|
|
|
public EnvironmentElement() => Create(withPreview: true);
|
|
public EnvironmentElement(bool withPreview, Action OnChangeCallback = null)
|
|
{
|
|
this.OnChangeCallback = OnChangeCallback;
|
|
Create(withPreview);
|
|
}
|
|
|
|
public EnvironmentElement(Environment environment)
|
|
{
|
|
Create(withPreview: true);
|
|
Bind(environment);
|
|
}
|
|
|
|
void Create(bool withPreview)
|
|
{
|
|
if (withPreview)
|
|
{
|
|
latlong = new Image();
|
|
latlong.style.width = k_SkyThumbnailWidth;
|
|
latlong.style.height = k_SkyThumbnailHeight;
|
|
Add(latlong);
|
|
}
|
|
|
|
environmentParams = GetDefaultInspector();
|
|
Add(environmentParams);
|
|
}
|
|
|
|
public void Bind(Environment environment)
|
|
{
|
|
this.environment = environment;
|
|
if (environment == null || environment.Equals(null))
|
|
return;
|
|
|
|
if (latlong != null && !latlong.Equals(null))
|
|
latlong.image = GetLatLongThumbnailTexture();
|
|
skyCubemapField.SetValueWithoutNotify(environment.cubemap);
|
|
skyRotationOffset.SetValueWithoutNotify(environment.rotation);
|
|
skyExposureField.SetValueWithoutNotify(environment.exposure);
|
|
sunPosition.SetValueWithoutNotify(new Vector2(environment.sunLongitude, environment.sunLatitude));
|
|
shadowColor.SetValueWithoutNotify(environment.shadowColor);
|
|
environmentName.SetValueWithoutNotify(environment.name);
|
|
}
|
|
|
|
public void Bind(Environment environment, Image deportedLatlong)
|
|
{
|
|
latlong = deportedLatlong;
|
|
Bind(environment);
|
|
}
|
|
|
|
static public Vector2 PositionToLatLong(Vector2 position)
|
|
{
|
|
Vector2 result = new Vector2();
|
|
result.x = position.y * Mathf.PI * 0.5f * Mathf.Rad2Deg;
|
|
result.y = (position.x * 0.5f + 0.5f) * 2f * Mathf.PI * Mathf.Rad2Deg;
|
|
|
|
if (result.x < -90.0f) result.x = -90f;
|
|
if (result.x > 90.0f) result.x = 90f;
|
|
|
|
return result;
|
|
}
|
|
|
|
public static void ResetToBrightestSpot(Environment environment)
|
|
{
|
|
cubeToLatlongMaterial.SetTexture("_MainTex", environment.cubemap);
|
|
cubeToLatlongMaterial.SetVector("_WindowParams", new Vector4(10000, -1000.0f, 2, 0.0f)); // Neutral value to not clip
|
|
cubeToLatlongMaterial.SetVector("_CubeToLatLongParams", new Vector4(Mathf.Deg2Rad * environment.rotation, 0.5f, 1.0f, 3.0f)); // We use LOD 3 to take a region rather than a single pixel in the map
|
|
cubeToLatlongMaterial.SetPass(0);
|
|
|
|
int width = k_SkyThumbnailWidth;
|
|
int height = width >> 1;
|
|
|
|
RenderTexture temporaryRT = new RenderTexture(width, height, 0, GraphicsFormat.R8G8B8A8_SRGB);
|
|
Texture2D brightestPointTexture = new Texture2D(width, height, GraphicsFormat.R16G16B16A16_SFloat, TextureCreationFlags.None);
|
|
|
|
// Convert cubemap to a 2D LatLong to read on CPU
|
|
Graphics.Blit(environment.cubemap, temporaryRT, cubeToLatlongMaterial);
|
|
brightestPointTexture.ReadPixels(new Rect(0, 0, width, height), 0, 0, false);
|
|
brightestPointTexture.Apply();
|
|
|
|
// CPU read back
|
|
// From Doc: The returned array is a flattened 2D array, where pixels are laid out left to right, bottom to top (i.e. row after row)
|
|
Color[] color = brightestPointTexture.GetPixels();
|
|
RenderTexture.active = null;
|
|
temporaryRT.Release();
|
|
|
|
float maxLuminance = 0.0f;
|
|
int maxIndex = 0;
|
|
for (int index = height * width - 1; index >= 0; --index)
|
|
{
|
|
Color pixel = color[index];
|
|
float luminance = pixel.r * 0.2126729f + pixel.g * 0.7151522f + pixel.b * 0.0721750f;
|
|
if (maxLuminance < luminance)
|
|
{
|
|
maxLuminance = luminance;
|
|
maxIndex = index;
|
|
}
|
|
}
|
|
Vector2 sunPosition = PositionToLatLong(new Vector2(((maxIndex % width) / (float)(width - 1)) * 2f - 1f, ((maxIndex / width) / (float)(height - 1)) * 2f - 1f));
|
|
environment.sunLatitude = sunPosition.x;
|
|
environment.sunLongitude = sunPosition.y - environment.rotation;
|
|
}
|
|
|
|
public Texture2D GetLatLongThumbnailTexture()
|
|
=> GetLatLongThumbnailTexture(environment, k_SkyThumbnailWidth);
|
|
|
|
public static Texture2D GetLatLongThumbnailTexture(Environment environment, int width)
|
|
{
|
|
int height = width >> 1;
|
|
RenderTexture oldActive = RenderTexture.active;
|
|
RenderTexture temporaryRT = new RenderTexture(width, height, 0, GraphicsFormat.R8G8B8A8_SRGB);
|
|
RenderTexture.active = temporaryRT;
|
|
cubeToLatlongMaterial.SetTexture("_MainTex", environment.cubemap);
|
|
cubeToLatlongMaterial.SetVector("_WindowParams",
|
|
new Vector4(
|
|
height, //height
|
|
-1000f, //y position, -1000f to be sure to not have clipping issue (we should not clip normally but don't want to create a new shader)
|
|
2f, //margin value
|
|
1f)); //Pixel per Point
|
|
cubeToLatlongMaterial.SetVector("_CubeToLatLongParams",
|
|
new Vector4(
|
|
Mathf.Deg2Rad * environment.rotation, //rotation of the environment in radian
|
|
1f, //alpha
|
|
1f, //intensity
|
|
0f)); //LOD
|
|
cubeToLatlongMaterial.SetPass(0);
|
|
GL.LoadPixelMatrix(0, width, height, 0);
|
|
GL.Clear(true, true, Color.black);
|
|
Rect skyRect = new Rect(0, 0, width, height);
|
|
Renderer.DrawFullScreenQuad(skyRect);
|
|
|
|
Texture2D result = new Texture2D(width, height, GraphicsFormat.R8G8B8A8_SRGB, TextureCreationFlags.None);
|
|
result.ReadPixels(new Rect(0, 0, width, height), 0, 0, false);
|
|
result.Apply(false);
|
|
RenderTexture.active = oldActive;
|
|
UnityEngine.Object.DestroyImmediate(temporaryRT);
|
|
return result;
|
|
}
|
|
|
|
public VisualElement GetDefaultInspector()
|
|
{
|
|
VisualElement inspector = new VisualElement() { name = "inspector" };
|
|
|
|
VisualElement header = new VisualElement() { name = "inspector-header" };
|
|
header.Add(new Image()
|
|
{
|
|
image = CoreEditorUtils.LoadIcon(@"Packages/com.unity.render-pipelines.core/Editor/LookDev/Icons/", "Environment", forceLowRes: true)
|
|
});
|
|
environmentName = new TextField();
|
|
environmentName.isDelayed = true;
|
|
environmentName.RegisterValueChangedCallback(evt =>
|
|
{
|
|
string path = AssetDatabase.GetAssetPath(environment);
|
|
environment.name = evt.newValue;
|
|
AssetDatabase.SetLabels(environment, new string[] { evt.newValue });
|
|
EditorUtility.SetDirty(environment);
|
|
AssetDatabase.ImportAsset(path);
|
|
environmentName.name = environment.name;
|
|
});
|
|
header.Add(environmentName);
|
|
inspector.Add(header);
|
|
|
|
Foldout foldout = new Foldout()
|
|
{
|
|
text = "Environment Settings"
|
|
};
|
|
skyCubemapField = new ObjectField("Sky with Sun")
|
|
{
|
|
tooltip = "A cubemap that will be used as the sky."
|
|
};
|
|
skyCubemapField.allowSceneObjects = false;
|
|
skyCubemapField.objectType = typeof(Cubemap);
|
|
skyCubemapField.RegisterValueChangedCallback(evt =>
|
|
{
|
|
var tmp = environment.cubemap;
|
|
RegisterChange(ref tmp, evt.newValue as Cubemap, updatePreview: true, customResync: () => environment.cubemap = tmp);
|
|
});
|
|
foldout.Add(skyCubemapField);
|
|
|
|
skyRotationOffset = new FloatField("Rotation")
|
|
{
|
|
tooltip = "Rotation offset on the longitude of the sky."
|
|
};
|
|
skyRotationOffset.RegisterValueChangedCallback(evt
|
|
=> RegisterChange(ref environment.rotation, Environment.ClampLongitude(evt.newValue), skyRotationOffset, updatePreview: true));
|
|
foldout.Add(skyRotationOffset);
|
|
|
|
skyExposureField = new FloatField("Exposure")
|
|
{
|
|
tooltip = "The exposure to apply with this sky."
|
|
};
|
|
skyExposureField.RegisterValueChangedCallback(evt
|
|
=> RegisterChange(ref environment.exposure, evt.newValue));
|
|
foldout.Add(skyExposureField);
|
|
var style = foldout.Q<Toggle>().style;
|
|
style.marginLeft = 3;
|
|
style.unityFontStyleAndWeight = FontStyle.Bold;
|
|
inspector.Add(foldout);
|
|
|
|
sunPosition = new Vector2Field("Sun Position")
|
|
{
|
|
tooltip = "The sun position as (Longitude, Latitude)\nThe button compute brightest position in the sky with sun."
|
|
};
|
|
sunPosition.Q("unity-x-input").Q<FloatField>().formatString = "n1";
|
|
sunPosition.Q("unity-y-input").Q<FloatField>().formatString = "n1";
|
|
sunPosition.RegisterValueChangedCallback(evt =>
|
|
{
|
|
var tmpContainer = new Vector2(
|
|
environment.sunLongitude,
|
|
environment.sunLatitude);
|
|
var tmpNewValue = new Vector2(
|
|
Environment.ClampLongitude(evt.newValue.x),
|
|
Environment.ClampLatitude(evt.newValue.y));
|
|
RegisterChange(ref tmpContainer, tmpNewValue, sunPosition, customResync: () =>
|
|
{
|
|
environment.sunLongitude = tmpContainer.x;
|
|
environment.sunLatitude = tmpContainer.y;
|
|
});
|
|
});
|
|
foldout.Add(sunPosition);
|
|
|
|
Button sunToBrightess = new Button(() =>
|
|
{
|
|
ResetToBrightestSpot(environment);
|
|
sunPosition.SetValueWithoutNotify(new Vector2(
|
|
Environment.ClampLongitude(environment.sunLongitude),
|
|
Environment.ClampLatitude(environment.sunLatitude)));
|
|
})
|
|
{
|
|
name = "sunToBrightestButton"
|
|
};
|
|
sunToBrightess.Add(new Image()
|
|
{
|
|
image = CoreEditorUtils.LoadIcon(@"Packages/com.unity.render-pipelines.core/Editor/LookDev/Icons/", "SunPosition", forceLowRes: true)
|
|
});
|
|
sunToBrightess.AddToClassList("sun-to-brightest-button");
|
|
var vector2Input = sunPosition.Q(className: "unity-vector2-field__input");
|
|
vector2Input.Remove(sunPosition.Q(className: "unity-composite-field__field-spacer"));
|
|
vector2Input.Add(sunToBrightess);
|
|
|
|
shadowColor = new ColorField("Shadow Tint")
|
|
{
|
|
tooltip = "The wanted shadow tint to be used when computing shadow."
|
|
};
|
|
shadowColor.RegisterValueChangedCallback(evt
|
|
=> RegisterChange(ref environment.shadowColor, evt.newValue));
|
|
foldout.Add(shadowColor);
|
|
|
|
style = foldout.Q<Toggle>().style;
|
|
style.marginLeft = 3;
|
|
style.unityFontStyleAndWeight = FontStyle.Bold;
|
|
inspector.Add(foldout);
|
|
|
|
return inspector;
|
|
}
|
|
|
|
void RegisterChange<TValueType>(ref TValueType reflectedVariable, TValueType newValue, BaseField<TValueType> resyncField = null, bool updatePreview = false, Action customResync = null)
|
|
{
|
|
if (environment == null || environment.Equals(null))
|
|
return;
|
|
reflectedVariable = newValue;
|
|
resyncField?.SetValueWithoutNotify(newValue);
|
|
customResync?.Invoke();
|
|
if (updatePreview && latlong != null && !latlong.Equals(null))
|
|
latlong.image = GetLatLongThumbnailTexture(environment, k_SkyThumbnailWidth);
|
|
EditorUtility.SetDirty(environment);
|
|
OnChangeCallback?.Invoke();
|
|
}
|
|
}
|
|
}
|