using UnityEngine;
using System;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
using System.Linq;
namespace UnityEditor.Rendering.LookDev
{
///
/// Lighting environment used in LookDev
///
public class Environment : ScriptableObject
{
[Serializable]
public abstract class BaseEnvironmentCubemapHandler
{
[SerializeField]
string m_CubemapGUID;
Cubemap m_Cubemap;
///
/// The cubemap used for this part of the lighting environment
///
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));
}
}
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(path);
}
}
}
///
/// Class containing editor data for shadow part of the lighting environment
///
[Serializable]
public class Shadow : BaseEnvironmentCubemapHandler
{
// 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[
///
/// The shading tint to used when computing shadow from sun
///
public Color color = Color.white;
///
/// The Latitude position of the sun casting shadows
///
public float sunLatitude
{
get => m_Latitude;
set => m_Latitude = ClampLatitude(value);
}
internal static float ClampLatitude(float value) => Mathf.Clamp(value, -90, 90);
///
/// The Longitude position of the sun casting shadows
///
public float sunLongitude
{
get => m_Longitude;
set => m_Longitude = ClampLongitude(value);
}
internal static float ClampLongitude(float value)
{
value = value % 360f;
if (value < 0.0)
value += 360f;
return value;
}
}
///
/// Class containing editor data for sky part of the lighting environment
///
[Serializable]
public class Sky : BaseEnvironmentCubemapHandler
{
///
/// Offset on the longitude. Affect both sky and sun position in Shadow part
///
public float rotation = 0.0f;
///
/// Exposure to use with this Sky
///
public float exposure = 1f;
///
/// Implicit conversion operator to runtime version of sky datas
///
/// Editor version of the datas
public static implicit operator UnityEngine.Rendering.LookDev.Sky(Sky sky)
=> sky == null
? default
: new UnityEngine.Rendering.LookDev.Sky()
{
cubemap = sky.cubemap,
longitudeOffset = sky.rotation,
exposure = sky.exposure
};
}
///
/// The sky part of the lighting environment
///
public Sky sky = new Sky();
///
/// The shadow part of the lighting environment
///
public Shadow shadow = new Shadow();
///
/// Compute the shadow runtime data with editor datas
///
public UnityEngine.Rendering.LookDev.Sky shadowSky
=> new UnityEngine.Rendering.LookDev.Sky()
{
cubemap = shadow.cubemap ?? sky.cubemap,
longitudeOffset = sky.rotation,
exposure = sky.exposure
};
internal float shadowIntensity
=> shadow.cubemap == null ? 0.3f : 1f;
internal void UpdateSunPosition(Light sun)
=> sun.transform.rotation = Quaternion.Euler(shadow.sunLatitude, sky.rotation + shadow.sunLongitude, 0f);
internal void CopyTo(Environment other)
{
other.sky.cubemap = sky.cubemap;
other.sky.exposure = sky.exposure;
other.sky.rotation = sky.rotation;
other.shadow.cubemap = shadow.cubemap;
other.shadow.sunLatitude = shadow.sunLatitude;
other.shadow.sunLongitude = shadow.sunLongitude;
other.shadow.color = shadow.color;
other.name = name + " (copy)";
}
///
/// Compute sun position to be brightest spot of the sky
///
public void ResetToBrightestSpot()
=> EnvironmentElement.ResetToBrightestSpot(this);
}
[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
{
void Bind(T data);
}
class EnvironmentElement : VisualElement, IBendable
{
internal const int k_SkyThumbnailWidth = 200;
internal const int k_SkyThumbnailHeight = 100;
const int k_SkadowThumbnailWidth = 60;
const int k_SkadowThumbnailHeight = 30;
const int k_SkadowThumbnailXPosition = 130;
const int k_SkadowThumbnailYPosition = 10;
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;
ObjectField shadowCubemapField;
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.sky.cubemap);
skyRotationOffset.SetValueWithoutNotify(environment.sky.rotation);
skyExposureField.SetValueWithoutNotify(environment.sky.exposure);
shadowCubemapField.SetValueWithoutNotify(environment.shadow.cubemap);
sunPosition.SetValueWithoutNotify(new Vector2(environment.shadow.sunLongitude, environment.shadow.sunLatitude));
shadowColor.SetValueWithoutNotify(environment.shadow.color);
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.sky.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.sky.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, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
Texture2D brightestPointTexture = new Texture2D(width, height, TextureFormat.RGBAHalf, false);
// Convert cubemap to a 2D LatLong to read on CPU
Graphics.Blit(environment.sky.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.shadow.sunLatitude = sunPosition.x;
environment.shadow.sunLongitude = sunPosition.y - environment.sky.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, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
RenderTexture.active = temporaryRT;
cubeToLatlongMaterial.SetTexture("_MainTex", environment.sky.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.sky.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);
if (environment.shadow.cubemap != null)
{
cubeToLatlongMaterial.SetTexture("_MainTex", environment.shadow.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.sky.rotation, //rotation of the environment in radian
1f, //alpha
0.3f, //intensity
0f)); //LOD
cubeToLatlongMaterial.SetPass(0);
int shadowWidth = (int)(width * (k_SkadowThumbnailWidth / (float)k_SkyThumbnailWidth));
int shadowXPosition = (int)(width * (k_SkadowThumbnailXPosition / (float)k_SkyThumbnailWidth));
int shadowYPosition = (int)(width * (k_SkadowThumbnailYPosition / (float)k_SkyThumbnailWidth));
Rect shadowRect = new Rect(
shadowXPosition,
shadowYPosition,
shadowWidth,
shadowWidth >> 1);
Renderer.DrawFullScreenQuad(shadowRect);
}
Texture2D result = new Texture2D(width, height, TextureFormat.ARGB32, false);
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.sky.cubemap;
RegisterChange(ref tmp, evt.newValue as Cubemap);
environment.sky.cubemap = tmp;
latlong.image = GetLatLongThumbnailTexture(environment, k_SkyThumbnailWidth);
});
foldout.Add(skyCubemapField);
shadowCubemapField = new ObjectField("Sky without Sun")
{
tooltip = "[Optional] A cubemap that will be used to compute self shadowing.\nIt should be the same sky without the sun.\nIf nothing is provided, the sky with sun will be used with lower intensity."
};
shadowCubemapField.allowSceneObjects = false;
shadowCubemapField.objectType = typeof(Cubemap);
shadowCubemapField.RegisterValueChangedCallback(evt =>
{
var tmp = environment.shadow.cubemap;
RegisterChange(ref tmp, evt.newValue as Cubemap);
environment.shadow.cubemap = tmp;
latlong.image = GetLatLongThumbnailTexture(environment, k_SkyThumbnailWidth);
});
foldout.Add(shadowCubemapField);
skyRotationOffset = new FloatField("Rotation")
{
tooltip = "Rotation offset on the longitude of the sky."
};
skyRotationOffset.RegisterValueChangedCallback(evt
=> RegisterChange(ref environment.sky.rotation, Environment.Shadow.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.sky.exposure, evt.newValue));
foldout.Add(skyExposureField);
var style = foldout.Q().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().formatString = "n1";
sunPosition.Q("unity-y-input").Q().formatString = "n1";
sunPosition.RegisterValueChangedCallback(evt =>
{
var tmpContainer = new Vector2(
environment.shadow.sunLongitude,
environment.shadow.sunLatitude);
var tmpNewValue = new Vector2(
Environment.Shadow.ClampLongitude(evt.newValue.x),
Environment.Shadow.ClampLatitude(evt.newValue.y));
RegisterChange(ref tmpContainer, tmpNewValue, sunPosition);
environment.shadow.sunLongitude = tmpContainer.x;
environment.shadow.sunLatitude = tmpContainer.y;
});
foldout.Add(sunPosition);
Button sunToBrightess = new Button(() =>
{
ResetToBrightestSpot(environment);
sunPosition.SetValueWithoutNotify(new Vector2(
Environment.Shadow.ClampLongitude(environment.shadow.sunLongitude),
Environment.Shadow.ClampLatitude(environment.shadow.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.shadow.color, evt.newValue));
foldout.Add(shadowColor);
style = foldout.Q().style;
style.marginLeft = 3;
style.unityFontStyleAndWeight = FontStyle.Bold;
inspector.Add(foldout);
return inspector;
}
void RegisterChange(ref TValueType reflectedVariable, TValueType newValue, BaseField resyncField = null, bool updatePreview = false)
{
if (environment == null || environment.Equals(null))
return;
reflectedVariable = newValue;
resyncField?.SetValueWithoutNotify(newValue);
if (updatePreview && latlong != null && !latlong.Equals(null))
latlong.image = GetLatLongThumbnailTexture(environment, k_SkyThumbnailWidth);
EditorUtility.SetDirty(environment);
OnChangeCallback?.Invoke();
}
}
}