This commit is contained in:
2021-06-13 10:28:03 +02:00
parent eb70603c85
commit df2d24cbd3
7487 changed files with 943244 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
sub-package.*
upm-ci~/**
.Editor/**
.yamato/**
*.zip*
TestRunnerOptions.json
.idea/**

View File

@@ -0,0 +1,190 @@
# Changelog
All notable changes to this package will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [11.0.0] - 2020-10-21
### Added
- Support for the PlayStation 5 platform has been added.
- Support for the XboxSeries platform has been added.
- New API in DynamicResolutionHandler to handle multicamera rendering for hardware mode. Changing cameras and resetting scaling per camera should be safe.
- New API functions with no side effects in DynamicResolutionHandler, to retrieve resolved drs scale and to apply DRS on a size.
### Fixed
- Fixed the default background color for previews to use the original color.
- Fixed a bug in FreeCamera which would only provide a speed boost for the first frame when pressing the Shfit key.
- Fixed spacing between property fields on the Volume Component Editors.
- Fixed ALL/NONE to maintain the state on the Volume Component Editors.
- Fixed the selection of the Additional properties from ALL/NONE when the option "Show additional properties" is disabled
- Fixed ACES tonemaping for Nintendo Switch by forcing some shader color conversion functions to full float precision.
- Fixed missing warning UI about Projector component being unsupported (case 1300327).
- Fixed the display name of a Volume Parameter when is defined the attribute InspectorName
- Fixed ACES tonemaping on mobile platforms by forcing some shader color conversion functions to full float precision.
- Fix crash on VolumeComponentWithQualityEditor when the current Pipeline is not HDRP
- Calculating correct rtHandleScale by considering the possible pixel rounding when DRS is on
## [10.2.0] - 2020-10-19
Version Updated
The version number for this package has increased due to a version update of a related graphics package.
## [10.1.0] - 2020-10-12
### Added
- Added context options "Move to Top", "Move to Bottom", "Expand All" and "Collapse All" for volume components.
- Added the support of input system V2
### Fixed
- Fixed the scene view to scale correctly when hardware dynamic resolution is enabled (case 1158661)
- Fixed game view artifacts on resizing when hardware dynamic resolution was enabled
### Changed
- LookDev menu item entry is now disabled if the current pipeline does not support it.
## [10.0.0] - 2019-06-10
### Added
- Add rough version of ContextualMenuDispatcher to solve conflict amongst SRP.
- Add api documentation for TextureCombiner.
- Add tooltips in LookDev's toolbar.
- Add XRGraphicsAutomatedTests helper class.
### Fixed
- Fixed compile errors for platforms with no VR support
- Replaced reference to Lightweight Render Pipeline by Universal Render Pipeline in the package description
- Fixed LighProbes when using LookDev.
- Fix LookDev minimal window size.
- Fix object rotation at instentiation to keep the one in prefab or used in hierarchy.
- Fixed shader compile errors when trying to use tessellation shaders with PlayStation VR on PS4.
- Fixed shader compile errors about LODDitheringTransition not being supported in GLES2.
- Fix `WaveIsFirstLane()` to ignore helper lanes in fragment shaders on PS4.
- Fixed a bug where Unity would crash if you tried to remove a Camera component from a GameObject using the Inspector window, while other components dependended on the Camera component.
- Fixed errors due to the debug menu when enabling the new input system.
- Fix LookDev FPS manipulation in view
- Fix LookDev zoom being stuck when going near camera pivot position
- Fix LookDev manipulation in view non responsive if directly using an HDRI
- Fix LookDev behaviour when user delete the EnvironmentLibrary asset
- Fix LookDev SunPosition button position
- Fix LookDev EnvironmentLibrary tab when asset is deleted
- Fix LookDev used Cubemap when asset is deleted
- Fixed the definition of `rcp()` for GLES2.
- Fixed copy/pasting of Volume Components when loading a new scene
- Fix LookDev issue when adding a GameObject containing a Volume into the LookDev's view.
- Fixed duplicated entry for com.unity.modules.xr in the runtime asmdef file
- Fixed the texture curve being destroyed from another thread than main (case 1211754)
- Fixed unreachable code in TextureXR.useTexArray
- Fixed GC pressure caused by `VolumeParameter<T>.GetHashCode()`
- Fixed issue when LookDev window is opened and the CoreRP Package is updated to a newer version.
- Fix LookDev's camera button layout.
- Fix LookDev's layout vanishing on domain reload.
- Fixed issue with the shader TransformWorldToHClipDir function computing the wrong result.
- Fixed division by zero in `V_SmithJointGGX` function.
- Fixed null reference exception in LookDev when setting the SRP to one not implementing LookDev (case 1245086)
- Fix LookDev's undo/redo on EnvironmentLibrary (case 1234725)
- Fix a compil error on OpenGL ES2 in directional lightmap sampling shader code
- Fix hierarchicalbox gizmo outside facing check in symetry or homothety mode no longer move the center
- Fix artifacts on Adreno 630 GPUs when using ACES Tonemapping
- Fixed a null ref in the volume component list when there is no volume components in the project.
- Fixed issue with volume manager trying to access a null volume.
- HLSL codegen will work with C# file using both the `GenerateHLSL` and C# 7 features.
### Changed
- Restored usage of ENABLE_VR to fix compilation errors on some platforms.
- Only call SetDirty on an object when actually modifying it in SRP updater utility
- Set depthSlice to -1 by default on SetRenderTarget() to clear all slices of Texture2DArray by default.
- ResourceReloader will now add additional InvalidImport check while it cannot load due to AssetDatabase not available.
- Replaced calls to deprecated PlayerSettings.virtualRealitySupported property.
- Enable RWTexture2D, RWTexture2DArray, RWTexture3D in gles 3.1
- Updated macros to be compatible with the new shader preprocessor.
- Updated shaders to be compatible with Microsoft's DXC.
- Changed CommandBufferPool.Get() to create an unnamed CommandBuffer. (No profiling markers)
## [7.1.1] - 2019-09-05
### Added
- Add separated debug mode in LookDev.
### Changed
- Replaced usage of ENABLE_VR in XRGraphics.cs by a version define (ENABLE_VR_MODULE) based on the presence of the built-in VR module
- `ResourceReloader` now works on non-public fields.
- Removed `normalize` from `UnpackNormalRGB` to match `UnpackNormalAG`.
- Fixed shadow routines compilation errors when "real" type is a typedef on "half".
- Removed debug menu in non development build.
## [7.0.1] - 2019-07-25
### Fixed
- Fixed a precision issue with the ACES tonemapper on mobile platforms.
## [7.0.0] - 2019-07-17
### Added
- First experimental version of the LookDev. Works with all SRP. Only branched on HDRP at the moment.
- LookDev out of experimental
## [6.7.0-preview] - 2019-05-16
## [6.6.0] - 2019-04-01
### Fixed
- Fixed compile errors in XRGraphics.cs when ENABLE_VR is not defined
## [6.5.0] - 2019-03-07
## [6.4.0] - 2019-02-21
### Added
- Enabled support for CBUFFER on OpenGL Core and OpenGL ES 3 backends.
## [6.3.0] - 2019-02-18
## [6.2.0] - 2019-02-15
## [6.1.0] - 2019-02-13
## [6.0.0] - 2019-02-23
### Fixed
- Fixed a typo in ERROR_ON_UNSUPPORTED_FUNCTION() that was causing the shader compiler to run out of memory in GLES2. [Case 1104271] (https://issuetracker.unity3d.com/issues/mobile-os-restarts-because-of-high-memory-usage-when-compiling-shaders-for-opengles2)
## [5.2.0] - 2018-11-27
## [5.1.0] - 2018-11-19
### Added
- Added a define for determining if any instancing path is taken.
### Changed
- The Core SRP package is no longer in preview.
## [5.0.0-preview] - 2018-10-18
### Changed
- XRGraphicConfig has been changed from a read-write control of XRSettings to XRGraphics, a read-only accessor to XRSettings. This improves consistency of XR behavior between the legacy render pipeline and SRP.
- XRGraphics members have been renamed to match XRSettings, and XRGraphics has been modified to only contain accessors potentially useful to SRP
- You can now have up to 16 additional shadow-casting lights.
### Fixed
- LWRP no longer executes shadow passes when there are no visible shadow casters in a Scene. Previously, this made the Scene render as too dark, overall.
## [4.0.0-preview] - 2018-09-28
### Added
- Space transform functions are now defined in `ShaderLibrary/SpaceTransforms.hlsl`.
### Changed
- Removed setting shader inclue path via old API, use package shader include paths
## [3.3.0] - 2018-01-01
## [3.2.0] - 2018-01-01
## [3.1.0] - 2018-01-01
### Added
- Add PCSS shadow filter
- Added Core EditMode tests
- Added Core unsafe utilities
### Improvements
- Improved volume UI & styling
- Fixed CoreUtils.QuickSort infinite loop when two elements in the list are equals.
### Changed
- Moved root files into folders for easier maintenance

View File

@@ -0,0 +1,123 @@
using UnityEngine;
using Object = UnityEngine.Object;
namespace UnityEditor.Rendering
{
/// <summary>
/// Utility functions for cameras in the editor.
/// </summary>
public static class CameraEditorUtils
{
/// <summary>Delegate that must give an initialized preview camera</summary>
/// <param name="sourceCamera">The initial Camera we want a preview from</param>
/// <param name="previewSize">The size of the preview</param>
/// <returns>The Preview camera, initialized</returns>
public delegate Camera GetPreviewCamera(Camera sourceCamera, Vector2 previewSize);
const float k_PreviewNormalizedSize = 0.2f;
internal static Material s_GUITextureBlit2SRGBMaterial;
/// <summary>
/// The material used to display a texture into SRGB
/// </summary>
public static Material GUITextureBlit2SRGBMaterial
{
get
{
if (!s_GUITextureBlit2SRGBMaterial)
{
Shader shader = EditorGUIUtility.LoadRequired("SceneView/GUITextureBlit2SRGB.shader") as Shader;
s_GUITextureBlit2SRGBMaterial = new Material(shader);
s_GUITextureBlit2SRGBMaterial.hideFlags = HideFlags.HideAndDontSave;
}
s_GUITextureBlit2SRGBMaterial.SetFloat("_ManualTex2SRGB", QualitySettings.activeColorSpace == ColorSpace.Linear ? 1.0f : 0.0f);
return s_GUITextureBlit2SRGBMaterial;
}
}
/// <summary>
/// Draw the overlay of a Camera
/// </summary>
/// <param name="target">The Camera that we want a preview</param>
/// <param name="sceneView">The scene view where to draw it</param>
/// <param name="previewCameraGetter">The way to get the preview camera corresponding to the target</param>
public static void DrawCameraSceneViewOverlay(Object target, SceneView sceneView, GetPreviewCamera previewCameraGetter)
{
if (target == null) return;
// cache some deep values
var c = (Camera)target;
var previewSize = Handles.GetMainGameViewSize();
if (previewSize.x < 0f)
{
// Fallback to Scene View of not a valid game view size
previewSize.x = sceneView.position.width;
previewSize.y = sceneView.position.height;
}
// Apply normalizedviewport rect of camera
var normalizedViewPortRect = c.rect;
previewSize.x *= Mathf.Max(normalizedViewPortRect.width, 0f);
previewSize.y *= Mathf.Max(normalizedViewPortRect.height, 0f);
// Prevent using invalid previewSize
if (previewSize.x <= 0f || previewSize.y <= 0f)
return;
var aspect = previewSize.x / previewSize.y;
// Scale down (fit to scene view)
previewSize.y = k_PreviewNormalizedSize * sceneView.position.height;
previewSize.x = previewSize.y * aspect;
if (previewSize.y > sceneView.position.height * 0.5f)
{
previewSize.y = sceneView.position.height * 0.5f;
previewSize.x = previewSize.y * aspect;
}
if (previewSize.x > sceneView.position.width * 0.5f)
{
previewSize.x = sceneView.position.width * 0.5f;
previewSize.y = previewSize.x / aspect;
}
// Get and reserve rect
Rect cameraRect = GUILayoutUtility.GetRect(previewSize.x, previewSize.y);
if (Event.current.type == EventType.Repaint)
{
var previewCamera = previewCameraGetter(c, previewSize);
if (previewCamera.targetTexture == null)
{
Debug.LogError("The preview camera must render in a render target");
return;
}
bool drawGizmo = sceneView.drawGizmos;
sceneView.drawGizmos = false;
previewCamera.Render();
sceneView.drawGizmos = drawGizmo;
Graphics.DrawTexture(cameraRect, previewCamera.targetTexture, new Rect(0, 0, 1, 1), 0, 0, 0, 0, GUI.color, GUITextureBlit2SRGBMaterial);
// We set target texture to null after this call otherwise if both sceneview and gameview are visible and we have a preview camera wwe
// get this error: "Releasing render texture that is set as Camera.targetTexture!"
previewCamera.targetTexture = null;
}
}
/// <summary>
/// Check if the view port rect have a positive size
/// </summary>
/// <param name="normalizedViewPortRect">The rect to check</param>
/// <returns>True: the rect have positive size</returns>
public static bool IsViewPortRectValidToRender(Rect normalizedViewPortRect)
{
if (normalizedViewPortRect.width <= 0f || normalizedViewPortRect.height <= 0f)
return false;
if (normalizedViewPortRect.x >= 1f || normalizedViewPortRect.xMax <= 0f)
return false;
if (normalizedViewPortRect.y >= 1f || normalizedViewPortRect.yMax <= 0f)
return false;
return true;
}
}
}

View File

@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace UnityEditor.Rendering
{
static class ContextualMenuDispatcher
{
[MenuItem("CONTEXT/Camera/Remove Component")]
static void RemoveCameraComponent(MenuCommand command)
{
Camera camera = command.context as Camera;
string error;
if (!DispatchRemoveComponent(camera))
{
//preserve built-in behavior
if (CanRemoveComponent(camera, out error))
Undo.DestroyObjectImmediate(command.context);
else
EditorUtility.DisplayDialog("Can't remove component", error, "Ok");
}
}
static bool DispatchRemoveComponent<T>(T component)
where T : Component
{
Type type = RenderPipelineEditorUtility.FetchFirstCompatibleTypeUsingScriptableRenderPipelineExtension<IRemoveAdditionalDataContextualMenu<T>>();
if (type != null)
{
IRemoveAdditionalDataContextualMenu<T> instance = (IRemoveAdditionalDataContextualMenu<T>)Activator.CreateInstance(type);
instance.RemoveComponent(component, ComponentDependencies(component));
return true;
}
return false;
}
static IEnumerable<Component> ComponentDependencies(Component component)
=> component.gameObject
.GetComponents<Component>()
.Where(c => c != component
&& c.GetType()
.GetCustomAttributes(typeof(RequireComponent), true)
.Count(att => att is RequireComponent rc
&& (rc.m_Type0 == component.GetType()
|| rc.m_Type1 == component.GetType()
|| rc.m_Type2 == component.GetType())) > 0);
static bool CanRemoveComponent(Component component, out string error)
{
var dependencies = ComponentDependencies(component);
if (dependencies.Count() == 0)
{
error = null;
return true;
}
Component firstDependency = dependencies.First();
error = $"Can't remove {component.GetType().Name} because {firstDependency.GetType().Name} depends on it.";
return false;
}
}
/// <summary>
/// Interface that should be used with [ScriptableRenderPipelineExtension(type))] attribute to dispatch ContextualMenu calls on the different SRPs
/// </summary>
/// <typeparam name="T">This must be a component that require AdditionalData in your SRP</typeparam>
public interface IRemoveAdditionalDataContextualMenu<T>
where T : Component
{
/// <summary>
/// Remove the given component
/// </summary>
/// <param name="component">The component to remove</param>
/// <param name="dependencies">Dependencies.</param>
void RemoveComponent(T component, IEnumerable<Component> dependencies);
}
}

View File

@@ -0,0 +1,584 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace UnityEditor.Rendering
{
/// <summary>display options added to the Foldout</summary>
[Flags]
public enum FoldoutOption
{
/// <summary>No Option</summary>
None = 0,
/// <summary>Foldout will be indented</summary>
Indent = 1 << 0,
/// <summary>Foldout will be boxed</summary>
Boxed = 1 << 2,
/// <summary>Foldout will be inside another foldout</summary>
SubFoldout = 1 << 3,
/// <summary>Remove the space at end of the foldout</summary>
NoSpaceAtEnd = 1 << 4
}
/// <summary>display options added to the Group</summary>
[Flags]
public enum GroupOption
{
/// <summary>No Option</summary>
None = 0,
/// <summary>Group will be indented</summary>
Indent = 1 << 0
}
/// <summary>
/// Utility class to draw inspectors
/// </summary>
/// <typeparam name="TData">Type of class containing data needed to draw inspector</typeparam>
public static class CoreEditorDrawer<TData>
{
/// <summary> Abstraction that have the Draw hability </summary>
public interface IDrawer
{
/// <summary>
/// The draw function
/// </summary>
/// <param name="serializedProperty">The SerializedProperty to draw</param>
/// <param name="owner">The editor handling this draw call</param>
void Draw(TData serializedProperty, Editor owner);
}
/// <summary>Delegate that must say if this is enabled for drawing</summary>
/// <param name="data">The data used</param>
/// <param name="owner">The editor rendering</param>
/// <returns>True if this should be drawn</returns>
public delegate bool Enabler(TData data, Editor owner);
/// <summary>Delegate is called when the foldout state is switched</summary>
/// <param name="data">The data used</param>
/// <param name="owner">The editor rendering</param>
public delegate void SwitchEnabler(TData data, Editor owner);
/// <summary>Delegate that must be used to select sub object for data for drawing</summary>
/// <typeparam name="T2Data">The type of the sub object used for data</typeparam>
/// <param name="data">The data used</param>
/// <param name="owner">The editor rendering</param>
/// <returns>Embeded object that will be used as data source for later draw in this Select</returns>
public delegate T2Data DataSelect<T2Data>(TData data, Editor owner);
/// <summary>Delegate type alternative to IDrawer</summary>
/// <param name="data">The data used</param>
/// <param name="owner">The editor rendering</param>
public delegate void ActionDrawer(TData data, Editor owner);
/// <summary> Equivalent to EditorGUILayout.Space that can be put in a drawer group </summary>
public static readonly IDrawer space = Group((data, owner) => EditorGUILayout.Space());
/// <summary> Use it when IDrawer required but no operation should be done </summary>
public static readonly IDrawer noop = Group((data, owner) => {});
/// <summary>
/// Conditioned drawer that will only be drawn if its enabler function is null or return true
/// </summary>
/// <param name="enabler">Enable the drawing if null or return true</param>
/// <param name="contentDrawers">The content of the group</param>
/// <returns>A IDrawer object</returns>
public static IDrawer Conditional(Enabler enabler, params IDrawer[] contentDrawers)
{
return new ConditionalDrawerInternal(enabler, contentDrawers.Draw);
}
/// <summary>
/// Conditioned drawer that will only be drawn if its enabler function is null or return true
/// </summary>
/// <param name="enabler">Enable the drawing if null or return true</param>
/// <param name="contentDrawers">The content of the group</param>
/// <returns>A IDrawer object</returns>
public static IDrawer Conditional(Enabler enabler, params ActionDrawer[] contentDrawers)
{
return new ConditionalDrawerInternal(enabler, contentDrawers);
}
class ConditionalDrawerInternal : IDrawer
{
ActionDrawer[] actionDrawers { get; set; }
Enabler m_Enabler;
public ConditionalDrawerInternal(Enabler enabler = null, params ActionDrawer[] actionDrawers)
{
this.actionDrawers = actionDrawers;
m_Enabler = enabler;
}
void IDrawer.Draw(TData data, Editor owner)
{
if (m_Enabler != null && !m_Enabler(data, owner))
return;
for (var i = 0; i < actionDrawers.Length; i++)
actionDrawers[i](data, owner);
}
}
/// <summary>
/// Conditioned drawer that will draw something depending of the return of the switch
/// </summary>
/// <param name="switch">Chose witch drawing to use</param>
/// <param name="drawIfTrue">This will be draw if the <see cref="switch"/> is true</param>
/// <param name="drawIfFalse">This will be draw if the <see cref="switch"/> is false</param>
/// <returns>A IDrawer object</returns>
public static IDrawer TernaryConditional(Enabler @switch, IDrawer drawIfTrue, IDrawer drawIfFalse)
=> new TernaryConditionalDrawerInternal(@switch, drawIfTrue.Draw, drawIfFalse.Draw);
/// <summary>
/// Conditioned drawer that will draw something depending of the return of the switch
/// </summary>
/// <param name="switch">Chose witch drawing to use</param>
/// <param name="drawIfTrue">This will be draw if the <see cref="switch"/> is true</param>
/// <param name="drawIfFalse">This will be draw if the <see cref="switch"/> is false</param>
/// <returns>A IDrawer object</returns>
public static IDrawer TernaryConditional(Enabler @switch, ActionDrawer drawIfTrue, ActionDrawer drawIfFalse)
=> new TernaryConditionalDrawerInternal(@switch, drawIfTrue, drawIfFalse);
class TernaryConditionalDrawerInternal : IDrawer
{
ActionDrawer drawIfTrue;
ActionDrawer drawIfFalse;
Enabler m_Switch;
public TernaryConditionalDrawerInternal(Enabler @switch, ActionDrawer drawIfTrue, ActionDrawer drawIfFalse)
{
this.drawIfTrue = drawIfTrue;
this.drawIfFalse = drawIfFalse;
m_Switch = @switch;
}
void IDrawer.Draw(TData data, Editor owner)
{
if (m_Switch != null && !m_Switch(data, owner))
drawIfFalse?.Invoke(data, owner);
else
drawIfTrue?.Invoke(data, owner);
}
}
/// <summary>
/// Group of drawing function for inspector.
/// They will be drawn one after the other.
/// </summary>
/// <param name="contentDrawers">The content of the group</param>
/// <returns>A IDrawer object</returns>
public static IDrawer Group(params IDrawer[] contentDrawers)
{
return new GroupDrawerInternal(-1f, GroupOption.None, contentDrawers.Draw);
}
/// <summary>
/// Group of drawing function for inspector.
/// They will be drawn one after the other.
/// </summary>
/// <param name="contentDrawers">The content of the group</param>
/// <returns>A IDrawer object</returns>
public static IDrawer Group(params ActionDrawer[] contentDrawers)
{
return new GroupDrawerInternal(-1f, GroupOption.None, contentDrawers);
}
/// <summary> Group of drawing function for inspector with a set width for labels </summary>
/// <param name="labelWidth">Width used for all labels in the group</param>
/// <param name="contentDrawers">The content of the group</param>
/// <returns>A IDrawer object</returns>
public static IDrawer Group(float labelWidth, params IDrawer[] contentDrawers)
{
return new GroupDrawerInternal(labelWidth, GroupOption.None, contentDrawers.Draw);
}
/// <summary> Group of drawing function for inspector with a set width for labels </summary>
/// <param name="labelWidth">Width used for all labels in the group</param>
/// <param name="contentDrawers">The content of the group</param>
/// <returns>A IDrawer object</returns>
public static IDrawer Group(float labelWidth, params ActionDrawer[] contentDrawers)
{
return new GroupDrawerInternal(labelWidth, GroupOption.None, contentDrawers);
}
/// <summary>
/// Group of drawing function for inspector.
/// They will be drawn one after the other.
/// </summary>
/// <param name="options">Allow to add indentation on this group</param>
/// <param name="contentDrawers">The content of the group</param>
/// <returns>A IDrawer object</returns>
public static IDrawer Group(GroupOption options, params IDrawer[] contentDrawers)
{
return new GroupDrawerInternal(-1f, options, contentDrawers.Draw);
}
/// <summary>
/// Group of drawing function for inspector.
/// They will be drawn one after the other.
/// </summary>
/// <param name="options">Allow to add indentation on this group</param>
/// <param name="contentDrawers">The content of the group</param>
/// <returns>A IDrawer object</returns>
public static IDrawer Group(GroupOption options, params ActionDrawer[] contentDrawers)
{
return new GroupDrawerInternal(-1f, options, contentDrawers);
}
/// <summary> Group of drawing function for inspector with a set width for labels </summary>
/// <param name="labelWidth">Width used for all labels in the group</param>
/// <param name="options">Allow to add indentation on this group</param>
/// <param name="contentDrawers">The content of the group</param>
/// <returns>A IDrawer object</returns>
public static IDrawer Group(float labelWidth, GroupOption options, params IDrawer[] contentDrawers)
{
return new GroupDrawerInternal(labelWidth, options, contentDrawers.Draw);
}
/// <summary> Group of drawing function for inspector with a set width for labels </summary>
/// <param name="labelWidth">Width used for all labels in the group</param>
/// <param name="options">Allow to add indentation on this group</param>
/// <param name="contentDrawers">The content of the group</param>
/// <returns>A IDrawer object</returns>
public static IDrawer Group(float labelWidth, GroupOption options, params ActionDrawer[] contentDrawers)
{
return new GroupDrawerInternal(labelWidth, options, contentDrawers);
}
class GroupDrawerInternal : IDrawer
{
ActionDrawer[] actionDrawers { get; set; }
float m_LabelWidth;
bool isIndented;
public GroupDrawerInternal(float labelWidth = -1f, GroupOption options = GroupOption.None, params ActionDrawer[] actionDrawers)
{
this.actionDrawers = actionDrawers;
m_LabelWidth = labelWidth;
isIndented = (options & GroupOption.Indent) != 0;
}
void IDrawer.Draw(TData data, Editor owner)
{
if (isIndented)
++EditorGUI.indentLevel;
var currentLabelWidth = EditorGUIUtility.labelWidth;
if (m_LabelWidth >= 0f)
{
EditorGUIUtility.labelWidth = m_LabelWidth;
}
for (var i = 0; i < actionDrawers.Length; i++)
actionDrawers[i](data, owner);
if (m_LabelWidth >= 0f)
{
EditorGUIUtility.labelWidth = currentLabelWidth;
}
if (isIndented)
--EditorGUI.indentLevel;
}
}
/// <summary> Create an IDrawer based on an other data container </summary>
/// <typeparam name="T2Data">Type of selected object containing in the given data containing data needed to draw inspector</typeparam>
/// <param name="dataSelect">The data new source for the inner drawers</param>
/// <param name="otherDrawers">Inner drawers drawed with given data sources</param>
/// <returns>A IDrawer object</returns>
public static IDrawer Select<T2Data>(
DataSelect<T2Data> dataSelect,
params CoreEditorDrawer<T2Data>.IDrawer[] otherDrawers)
{
return new SelectDrawerInternal<T2Data>(dataSelect, otherDrawers.Draw);
}
/// <summary> Create an IDrawer based on an other data container </summary>
/// <typeparam name="T2Data">Type of selected object containing in the given data containing data needed to draw inspector</typeparam>
/// <param name="dataSelect">The data new source for the inner drawers</param>
/// <param name="otherDrawers">Inner drawers drawed with given data sources</param>
/// <returns>A IDrawer object</returns>
public static IDrawer Select<T2Data>(
DataSelect<T2Data> dataSelect,
params CoreEditorDrawer<T2Data>.ActionDrawer[] otherDrawers)
{
return new SelectDrawerInternal<T2Data>(dataSelect, otherDrawers);
}
class SelectDrawerInternal<T2Data> : IDrawer
{
DataSelect<T2Data> m_DataSelect;
CoreEditorDrawer<T2Data>.ActionDrawer[] m_SourceDrawers;
public SelectDrawerInternal(DataSelect<T2Data> dataSelect,
params CoreEditorDrawer<T2Data>.ActionDrawer[] otherDrawers)
{
m_SourceDrawers = otherDrawers;
m_DataSelect = dataSelect;
}
void IDrawer.Draw(TData data, Editor o)
{
var p2 = m_DataSelect(data, o);
for (var i = 0; i < m_SourceDrawers.Length; i++)
m_SourceDrawers[i](p2, o);
}
}
/// <summary>
/// Create an IDrawer foldout header using an ExpandedState.
/// The default option is Indent in this version.
/// </summary>
/// <typeparam name="TEnum">Type of the mask used</typeparam>
/// <typeparam name="TState">Type of the persistent state</typeparam>
/// <param name="title">Title wanted for this foldout header</param>
/// <param name="mask">Bit mask (enum) used to define the boolean saving the state in ExpandedState</param>
/// <param name="state">The ExpandedState describing the component</param>
/// <param name="contentDrawers">The content of the foldout header</param>
/// <returns>A IDrawer object</returns>
public static IDrawer FoldoutGroup<TEnum, TState>(string title, TEnum mask, ExpandedState<TEnum, TState> state, params IDrawer[] contentDrawers)
where TEnum : struct, IConvertible
{
return FoldoutGroup(title, mask, state, contentDrawers.Draw);
}
/// <summary>
/// Create an IDrawer foldout header using an ExpandedState.
/// The default option is Indent in this version.
/// </summary>
/// <typeparam name="TEnum">Type of the mask used</typeparam>
/// <typeparam name="TState">Type of the persistent state</typeparam>
/// <param name="title">Title wanted for this foldout header</param>
/// <param name="mask">Bit mask (enum) used to define the boolean saving the state in ExpandedState</param>
/// <param name="state">The ExpandedState describing the component</param>
/// <param name="contentDrawers">The content of the foldout header</param>
/// <returns>A IDrawer object</returns>
public static IDrawer FoldoutGroup<TEnum, TState>(string title, TEnum mask, ExpandedState<TEnum, TState> state, params ActionDrawer[] contentDrawers)
where TEnum : struct, IConvertible
{
return FoldoutGroup(EditorGUIUtility.TrTextContent(title), mask, state, contentDrawers);
}
/// <summary> Create an IDrawer foldout header using an ExpandedState </summary>
/// <typeparam name="TEnum">Type of the mask used</typeparam>
/// <typeparam name="TState">Type of the persistent state</typeparam>
/// <param name="title">Title wanted for this foldout header</param>
/// <param name="mask">Bit mask (enum) used to define the boolean saving the state in ExpandedState</param>
/// <param name="state">The ExpandedState describing the component</param>
/// <param name="options">Drawing options</param>
/// <param name="contentDrawers">The content of the foldout header</param>
/// <returns>A IDrawer object</returns>
public static IDrawer FoldoutGroup<TEnum, TState>(string title, TEnum mask, ExpandedState<TEnum, TState> state, FoldoutOption options, params IDrawer[] contentDrawers)
where TEnum : struct, IConvertible
{
return FoldoutGroup(title, mask, state, options, contentDrawers.Draw);
}
/// <summary> Create an IDrawer foldout header using an ExpandedState </summary>
/// <typeparam name="TEnum">Type of the mask used</typeparam>
/// <typeparam name="TState">Type of the persistent state</typeparam>
/// <param name="title">Title wanted for this foldout header</param>
/// <param name="mask">Bit mask (enum) used to define the boolean saving the state in ExpandedState</param>
/// <param name="state">The ExpandedState describing the component</param>
/// <param name="options">Drawing options</param>
/// <param name="contentDrawers">The content of the foldout header</param>
/// <returns>A IDrawer object</returns>
public static IDrawer FoldoutGroup<TEnum, TState>(string title, TEnum mask, ExpandedState<TEnum, TState> state, FoldoutOption options, params ActionDrawer[] contentDrawers)
where TEnum : struct, IConvertible
{
return FoldoutGroup(EditorGUIUtility.TrTextContent(title), mask, state, options, contentDrawers);
}
/// <summary>
/// Create an IDrawer foldout header using an ExpandedState.
/// The default option is Indent in this version.
/// </summary>
/// <typeparam name="TEnum">Type of the mask used</typeparam>
/// <typeparam name="TState">Type of the persistent state</typeparam>
/// <param name="title">Title wanted for this foldout header</param>
/// <param name="mask">Bit mask (enum) used to define the boolean saving the state in ExpandedState</param>
/// <param name="state">The ExpandedState describing the component</param>
/// <param name="contentDrawers">The content of the foldout header</param>
/// <returns>A IDrawer object</returns>
public static IDrawer FoldoutGroup<TEnum, TState>(GUIContent title, TEnum mask, ExpandedState<TEnum, TState> state, params IDrawer[] contentDrawers)
where TEnum : struct, IConvertible
{
return FoldoutGroup(title, mask, state, contentDrawers.Draw);
}
/// <summary>
/// Create an IDrawer foldout header using an ExpandedState.
/// The default option is Indent in this version.
/// </summary>
/// <typeparam name="TEnum">Type of the mask used</typeparam>
/// <typeparam name="TState">Type of the persistent state</typeparam>
/// <param name="title">Title wanted for this foldout header</param>
/// <param name="mask">Bit mask (enum) used to define the boolean saving the state in ExpandedState</param>
/// <param name="state">The ExpandedState describing the component</param>
/// <param name="contentDrawers">The content of the foldout header</param>
/// <returns>A IDrawer object</returns>
public static IDrawer FoldoutGroup<TEnum, TState>(GUIContent title, TEnum mask, ExpandedState<TEnum, TState> state, params ActionDrawer[] contentDrawers)
where TEnum : struct, IConvertible
{
return FoldoutGroup(title, mask, state, FoldoutOption.Indent, contentDrawers);
}
/// <summary> Create an IDrawer foldout header using an ExpandedState </summary>
/// <typeparam name="TEnum">Type of the mask used</typeparam>
/// <typeparam name="TState">Type of the persistent state</typeparam>
/// <param name="title">Title wanted for this foldout header</param>
/// <param name="mask">Bit mask (enum) used to define the boolean saving the state in ExpandedState</param>
/// <param name="state">The ExpandedState describing the component</param>
/// <param name="options">Drawing options</param>
/// <param name="contentDrawers">The content of the foldout header</param>
/// <returns>A IDrawer object</returns>
public static IDrawer FoldoutGroup<TEnum, TState>(GUIContent title, TEnum mask, ExpandedState<TEnum, TState> state, FoldoutOption options, params IDrawer[] contentDrawers)
where TEnum : struct, IConvertible
{
return FoldoutGroup(title, mask, state, options, contentDrawers.Draw);
}
/// <summary> Create an IDrawer foldout header using an ExpandedState </summary>
/// <typeparam name="TEnum">Type of the mask used</typeparam>
/// <typeparam name="TState">Type of the persistent state</typeparam>
/// <param name="title">Title wanted for this foldout header</param>
/// <param name="mask">Bit mask (enum) used to define the boolean saving the state in ExpandedState</param>
/// <param name="state">The ExpandedState describing the component</param>
/// <param name="options">Drawing options</param>
/// <param name="contentDrawers">The content of the foldout header</param>
/// <returns>A IDrawer object</returns>
public static IDrawer FoldoutGroup<TEnum, TState>(GUIContent title, TEnum mask, ExpandedState<TEnum, TState> state, FoldoutOption options, params ActionDrawer[] contentDrawers)
where TEnum : struct, IConvertible
{
return FoldoutGroup(title, mask, state, options, null, null, contentDrawers);
}
// This one is private as we do not want to have unhandled advanced switch. Change it if necessary.
static IDrawer FoldoutGroup<TEnum, TState>(GUIContent title, TEnum mask, ExpandedState<TEnum, TState> state, FoldoutOption options, Enabler isAdvanced, SwitchEnabler switchAdvanced, params ActionDrawer[] contentDrawers)
where TEnum : struct, IConvertible
{
return Group((data, owner) =>
{
bool isBoxed = (options & FoldoutOption.Boxed) != 0;
bool isIndented = (options & FoldoutOption.Indent) != 0;
bool isSubFoldout = (options & FoldoutOption.SubFoldout) != 0;
bool noSpaceAtEnd = (options & FoldoutOption.NoSpaceAtEnd) != 0;
bool expended = state[mask];
bool newExpended = expended;
if (isSubFoldout)
{
newExpended = CoreEditorUtils.DrawSubHeaderFoldout(title, expended, isBoxed,
isAdvanced == null ? (Func<bool>)null : () => isAdvanced(data, owner),
switchAdvanced == null ? (Action)null : () => switchAdvanced(data, owner));
}
else
{
CoreEditorUtils.DrawSplitter(isBoxed);
newExpended = CoreEditorUtils.DrawHeaderFoldout(title, expended, isBoxed,
isAdvanced == null ? (Func<bool>)null : () => isAdvanced(data, owner),
switchAdvanced == null ? (Action)null : () => switchAdvanced(data, owner));
}
if (newExpended ^ expended)
state[mask] = newExpended;
if (newExpended)
{
if (isIndented)
++EditorGUI.indentLevel;
for (var i = 0; i < contentDrawers.Length; i++)
contentDrawers[i](data, owner);
if (isIndented)
--EditorGUI.indentLevel;
if (!noSpaceAtEnd)
EditorGUILayout.Space();
}
});
}
/// <summary> Helper to draw a foldout with an advanced switch on it. </summary>
/// <typeparam name="TEnum">Type of the mask used</typeparam>
/// <typeparam name="TState">Type of the persistent state</typeparam>
/// <param name="foldoutTitle">Title wanted for this foldout header</param>
/// <param name="foldoutMask">Bit mask (enum) used to define the boolean saving the state in ExpandedState</param>
/// <param name="foldoutState">The ExpandedState describing the component</param>
/// <param name="isAdvanced"> Delegate allowing to check if advanced mode is active. </param>
/// <param name="switchAdvanced"> Delegate to know what to do when advance is switched. </param>
/// <param name="normalContent"> The content of the foldout header always visible if expended. </param>
/// <param name="advancedContent"> The content of the foldout header only visible if advanced mode is active and if foldout is expended. </param>
/// <param name="options">Drawing options</param>
/// <returns>A IDrawer object</returns>
public static IDrawer AdvancedFoldoutGroup<TEnum, TState>(GUIContent foldoutTitle, TEnum foldoutMask, ExpandedState<TEnum, TState> foldoutState, Enabler isAdvanced, SwitchEnabler switchAdvanced, IDrawer normalContent, IDrawer advancedContent, FoldoutOption options = FoldoutOption.Indent)
where TEnum : struct, IConvertible
{
return AdvancedFoldoutGroup(foldoutTitle, foldoutMask, foldoutState, isAdvanced, switchAdvanced, normalContent.Draw, advancedContent.Draw, options);
}
/// <summary> Helper to draw a foldout with an advanced switch on it. </summary>
/// <typeparam name="TEnum">Type of the mask used</typeparam>
/// <typeparam name="TState">Type of the persistent state</typeparam>
/// <param name="foldoutTitle">Title wanted for this foldout header</param>
/// <param name="foldoutMask">Bit mask (enum) used to define the boolean saving the state in ExpandedState</param>
/// <param name="foldoutState">The ExpandedState describing the component</param>
/// <param name="isAdvanced"> Delegate allowing to check if advanced mode is active. </param>
/// <param name="switchAdvanced"> Delegate to know what to do when advance is switched. </param>
/// <param name="normalContent"> The content of the foldout header always visible if expended. </param>
/// <param name="advancedContent"> The content of the foldout header only visible if advanced mode is active and if foldout is expended. </param>
/// <param name="options">Drawing options</param>
/// <returns>A IDrawer object</returns>
public static IDrawer AdvancedFoldoutGroup<TEnum, TState>(GUIContent foldoutTitle, TEnum foldoutMask, ExpandedState<TEnum, TState> foldoutState, Enabler isAdvanced, SwitchEnabler switchAdvanced, ActionDrawer normalContent, IDrawer advancedContent, FoldoutOption options = FoldoutOption.Indent)
where TEnum : struct, IConvertible
{
return AdvancedFoldoutGroup(foldoutTitle, foldoutMask, foldoutState, isAdvanced, switchAdvanced, normalContent, advancedContent.Draw, options);
}
/// <summary> Helper to draw a foldout with an advanced switch on it. </summary>
/// <typeparam name="TEnum">Type of the mask used</typeparam>
/// <typeparam name="TState">Type of the persistent state</typeparam>
/// <param name="foldoutTitle">Title wanted for this foldout header</param>
/// <param name="foldoutMask">Bit mask (enum) used to define the boolean saving the state in ExpandedState</param>
/// <param name="foldoutState">The ExpandedState describing the component</param>
/// <param name="isAdvanced"> Delegate allowing to check if advanced mode is active. </param>
/// <param name="switchAdvanced"> Delegate to know what to do when advance is switched. </param>
/// <param name="normalContent"> The content of the foldout header always visible if expended. </param>
/// <param name="advancedContent"> The content of the foldout header only visible if advanced mode is active and if foldout is expended. </param>
/// <param name="options">Drawing options</param>
/// <returns>A IDrawer object</returns>
public static IDrawer AdvancedFoldoutGroup<TEnum, TState>(GUIContent foldoutTitle, TEnum foldoutMask, ExpandedState<TEnum, TState> foldoutState, Enabler isAdvanced, SwitchEnabler switchAdvanced, IDrawer normalContent, ActionDrawer advancedContent, FoldoutOption options = FoldoutOption.Indent)
where TEnum : struct, IConvertible
{
return AdvancedFoldoutGroup(foldoutTitle, foldoutMask, foldoutState, isAdvanced, switchAdvanced, normalContent.Draw, advancedContent, options);
}
/// <summary> Helper to draw a foldout with an advanced switch on it. </summary>
/// <typeparam name="TEnum">Type of the mask used</typeparam>
/// <typeparam name="TState">Type of the persistent state</typeparam>
/// <param name="foldoutTitle">Title wanted for this foldout header</param>
/// <param name="foldoutMask">Bit mask (enum) used to define the boolean saving the state in ExpandedState</param>
/// <param name="foldoutState">The ExpandedState describing the component</param>
/// <param name="isAdvanced"> Delegate allowing to check if advanced mode is active. </param>
/// <param name="switchAdvanced"> Delegate to know what to do when advance is switched. </param>
/// <param name="normalContent"> The content of the foldout header always visible if expended. </param>
/// <param name="advancedContent"> The content of the foldout header only visible if advanced mode is active and if foldout is expended. </param>
/// <param name="options">Drawing options</param>
/// <returns>A IDrawer object</returns>
public static IDrawer AdvancedFoldoutGroup<TEnum, TState>(GUIContent foldoutTitle, TEnum foldoutMask, ExpandedState<TEnum, TState> foldoutState, Enabler isAdvanced, SwitchEnabler switchAdvanced, ActionDrawer normalContent, ActionDrawer advancedContent, FoldoutOption options = FoldoutOption.Indent)
where TEnum : struct, IConvertible
{
return FoldoutGroup(foldoutTitle, foldoutMask, foldoutState, options, isAdvanced, switchAdvanced,
normalContent,
Conditional((serialized, owner) => isAdvanced(serialized, owner) && foldoutState[foldoutMask], advancedContent).Draw
);
}
}
/// <summary>CoreEditorDrawer extensions</summary>
public static class CoreEditorDrawersExtensions
{
/// <summary> Concatenate a collection of IDrawer as a unique IDrawer </summary>
/// <typeparam name="TData">Type of class containing data needed to draw inspector</typeparam>
/// <param name="drawers">A collection of IDrawers</param>
/// <param name="data">The data source for the inner drawers</param>
/// <param name="owner">The editor drawing</param>
public static void Draw<TData>(this IEnumerable<CoreEditorDrawer<TData>.IDrawer> drawers, TData data, Editor owner)
{
foreach (var drawer in drawers)
drawer.Draw(data, owner);
}
}
}

View File

@@ -0,0 +1,51 @@
using UnityEngine;
namespace UnityEditor.Rendering
{
/// <summary>Class containing style definition</summary>
public static class CoreEditorStyles
{
/// <summary>Style for a small checkbox</summary>
public static readonly GUIStyle smallTickbox;
/// <summary>Style for a small checkbox in mixed state</summary>
public static readonly GUIStyle smallMixedTickbox;
/// <summary>Style for a minilabel button</summary>
public static readonly GUIStyle miniLabelButton;
static readonly Texture2D paneOptionsIconDark;
static readonly Texture2D paneOptionsIconLight;
/// <summary> PaneOption icon </summary>
public static Texture2D paneOptionsIcon { get { return EditorGUIUtility.isProSkin ? paneOptionsIconDark : paneOptionsIconLight; } }
static CoreEditorStyles()
{
smallTickbox = new GUIStyle("ShurikenToggle");
smallMixedTickbox = new GUIStyle("ShurikenToggleMixed");
var transparentTexture = new Texture2D(1, 1, TextureFormat.ARGB32, false);
transparentTexture.SetPixel(0, 0, Color.clear);
transparentTexture.Apply();
miniLabelButton = new GUIStyle(EditorStyles.miniLabel);
miniLabelButton.normal = new GUIStyleState
{
background = transparentTexture,
scaledBackgrounds = null,
textColor = Color.grey
};
var activeState = new GUIStyleState
{
background = transparentTexture,
scaledBackgrounds = null,
textColor = Color.white
};
miniLabelButton.active = activeState;
miniLabelButton.onNormal = activeState;
miniLabelButton.onActive = activeState;
paneOptionsIconDark = (Texture2D)EditorGUIUtility.Load("Builtin Skins/DarkSkin/Images/pane options.png");
paneOptionsIconLight = (Texture2D)EditorGUIUtility.Load("Builtin Skins/LightSkin/Images/pane options.png");
}
}
}

View File

@@ -0,0 +1,184 @@
using System;
using UnityEngine;
using UnityEngine.Rendering;
namespace UnityEditor.Rendering
{
/// <summary>
/// Serialized state of a Debug Item.
/// </summary>
[Serializable]
public abstract class DebugState : ScriptableObject
{
/// <summary>
/// Path of the Debug Item.
/// </summary>
[SerializeField]
protected string m_QueryPath;
// We need this to keep track of the state modified in the current frame.
// This helps reduces the cost of re-applying states to original widgets and is also needed
// when two states point to the same value (e.g. when using split enums like HDRP does for
// the `fullscreenDebugMode`.
internal static DebugState m_CurrentDirtyState;
/// <summary>
/// Path of the Debug Item.
/// </summary>
public string queryPath
{
get { return m_QueryPath; }
internal set { m_QueryPath = value; }
}
/// <summary>
/// Returns the value of the Debug Item.
/// </summary>
/// <returns>Value of the Debug Item.</returns>
public abstract object GetValue();
/// <summary>
/// Set the value of the Debug Item.
/// </summary>
/// <param name="value">Input value.</param>
/// <param name="field">Debug Item field.</param>
public abstract void SetValue(object value, DebugUI.IValueField field);
/// <summary>
/// OnEnable implementation.
/// </summary>
public virtual void OnEnable()
{
hideFlags = HideFlags.HideAndDontSave;
}
}
/// <summary>
/// Generic serialized state of a Debug Item.
/// </summary>
/// <typeparam name="T"></typeparam>
[Serializable]
public class DebugState<T> : DebugState
{
/// <summary>
/// Value of the Debug Item.
/// </summary>
[SerializeField]
protected T m_Value;
/// <summary>
/// Value of the Debug Item
/// </summary>
public virtual T value
{
get { return m_Value; }
set { m_Value = value; }
}
/// <summary>
/// Returns the value of the Debug Item.
/// </summary>
/// <returns>Value of the Debug Item.</returns>
public override object GetValue()
{
return value;
}
/// <summary>
/// Set the value of the Debug Item.
/// </summary>
/// <param name="value">Input value.</param>
/// <param name="field">Debug Item field.</param>
public override void SetValue(object value, DebugUI.IValueField field)
{
this.value = (T)field.ValidateValue(value);
}
/// <summary>
/// Returns the hash code of the Debug Item.
/// </summary>
/// <returns>Hash code of the Debug Item</returns>
public override int GetHashCode()
{
unchecked
{
int hash = 13;
hash = hash * 23 + m_QueryPath.GetHashCode();
hash = hash * 23 + m_Value.GetHashCode();
return hash;
}
}
}
/// <summary>
/// Attribute specifying which types should be save as this Debug State.
/// </summary>
public sealed class DebugStateAttribute : Attribute
{
internal readonly Type[] types;
/// <summary>
/// Debug State Attribute constructor
/// </summary>
/// <param name="types">List of types of the Debug State.</param>
public DebugStateAttribute(params Type[] types)
{
this.types = types;
}
}
// Builtins
/// <summary>
/// Boolean Debug State.
/// </summary>
[Serializable, DebugState(typeof(DebugUI.BoolField), typeof(DebugUI.Foldout), typeof(DebugUI.HistoryBoolField))]
public sealed class DebugStateBool : DebugState<bool> {}
/// <summary>
/// Integer Debug State.
/// </summary>
[Serializable, DebugState(typeof(DebugUI.IntField), typeof(DebugUI.EnumField), typeof(DebugUI.HistoryEnumField))]
public sealed class DebugStateInt : DebugState<int> {}
/// <summary>
/// Flags Debug State.
/// </summary>
[Serializable, DebugState(typeof(DebugUI.BitField))]
public sealed class DebugStateFlags : DebugState<Enum> {}
/// <summary>
/// Unsigned Integer Debug State.
/// </summary>
[Serializable, DebugState(typeof(DebugUI.UIntField))]
public sealed class DebugStateUInt : DebugState<uint> {}
/// <summary>
/// Float Debug State.
/// </summary>
[Serializable, DebugState(typeof(DebugUI.FloatField))]
public sealed class DebugStateFloat : DebugState<float> {}
/// <summary>
/// Color Debug State.
/// </summary>
[Serializable, DebugState(typeof(DebugUI.ColorField))]
public sealed class DebugStateColor : DebugState<Color> {}
/// <summary>
/// Vector2 Debug State.
/// </summary>
[Serializable, DebugState(typeof(DebugUI.Vector2Field))]
public sealed class DebugStateVector2 : DebugState<Vector2> {}
/// <summary>
/// Vector3 Debug State.
/// </summary>
[Serializable, DebugState(typeof(DebugUI.Vector3Field))]
public sealed class DebugStateVector3 : DebugState<Vector3> {}
/// <summary>
/// Vector4 Debug State.
/// </summary>
[Serializable, DebugState(typeof(DebugUI.Vector4Field))]
public sealed class DebugStateVector4 : DebugState<Vector4> {}
}

View File

@@ -0,0 +1,758 @@
using System;
using UnityEngine;
using UnityEngine.Rendering;
namespace UnityEditor.Rendering
{
/// <summary>
/// Builtin Drawer for Value Debug Items.
/// </summary>
[DebugUIDrawer(typeof(DebugUI.Value))]
public sealed class DebugUIDrawerValue : DebugUIDrawer
{
/// <summary>
/// OnGUI implementation for Value DebugUIDrawer.
/// </summary>
/// <param name="widget">DebugUI Widget.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
/// <returns>The state of the widget.</returns>
public override bool OnGUI(DebugUI.Widget widget, DebugState state)
{
var w = Cast<DebugUI.Value>(widget);
var rect = PrepareControlRect();
EditorGUI.LabelField(rect, EditorGUIUtility.TrTextContent(w.displayName), EditorGUIUtility.TrTextContent(w.GetValue().ToString()));
return true;
}
}
/// <summary>
/// Builtin Drawer for Button Debug Items.
/// </summary>
[DebugUIDrawer(typeof(DebugUI.Button))]
public sealed class DebugUIDrawerButton : DebugUIDrawer
{
/// <summary>
/// OnGUI implementation for Button DebugUIDrawer.
/// </summary>
/// <param name="widget">DebugUI Widget.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
/// <returns>The state of the widget.</returns>
public override bool OnGUI(DebugUI.Widget widget, DebugState state)
{
var w = Cast<DebugUI.Button>(widget);
var rect = EditorGUI.IndentedRect(EditorGUILayout.GetControlRect());
if (GUI.Button(rect, w.displayName, EditorStyles.miniButton))
{
if (w.action != null)
w.action();
}
return true;
}
}
/// <summary>
/// Builtin Drawer for Boolean Debug Items.
/// </summary>
[DebugUIDrawer(typeof(DebugUI.BoolField))]
public sealed class DebugUIDrawerBoolField : DebugUIDrawer
{
/// <summary>
/// OnGUI implementation for Boolean DebugUIDrawer.
/// </summary>
/// <param name="widget">DebugUI Widget.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
/// <returns>The state of the widget.</returns>
public override bool OnGUI(DebugUI.Widget widget, DebugState state)
{
var w = Cast<DebugUI.BoolField>(widget);
var s = Cast<DebugStateBool>(state);
EditorGUI.BeginChangeCheck();
var rect = PrepareControlRect();
bool value = EditorGUI.Toggle(rect, EditorGUIUtility.TrTextContent(w.displayName), w.GetValue());
if (EditorGUI.EndChangeCheck())
Apply(w, s, value);
return true;
}
}
/// <summary>
/// Builtin Drawer for History Boolean Debug Items.
/// </summary>
[DebugUIDrawer(typeof(DebugUI.HistoryBoolField))]
public sealed class DebugUIDrawerHistoryBoolField : DebugUIDrawer
{
/// <summary>
/// OnGUI implementation for History Boolean DebugUIDrawer.
/// </summary>
/// <param name="widget">DebugUI Widget.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
/// <returns>The state of the widget.</returns>
public override bool OnGUI(DebugUI.Widget widget, DebugState state)
{
var w = Cast<DebugUI.HistoryBoolField>(widget);
var s = Cast<DebugStateBool>(state);
EditorGUI.BeginChangeCheck();
var rect = PrepareControlRect();
var labelRect = rect;
labelRect.width = EditorGUIUtility.labelWidth;
const int oneValueWidth = 70;
var valueRects = new Rect[w.historyDepth + 1];
for (int i = 0; i < w.historyDepth + 1; i++)
{
valueRects[i] = rect;
valueRects[i].x += EditorGUIUtility.labelWidth + i * oneValueWidth;
valueRects[i].width = oneValueWidth;
}
EditorGUI.LabelField(labelRect, EditorGUIUtility.TrTextContent(w.displayName));
int indent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0; //be at left of rects
bool value = EditorGUI.Toggle(valueRects[0], w.GetValue());
using (new EditorGUI.DisabledScope(true))
{
for (int i = 0; i < w.historyDepth; i++)
EditorGUI.Toggle(valueRects[i + 1], w.GetHistoryValue(i));
}
EditorGUI.indentLevel = indent;
if (EditorGUI.EndChangeCheck())
Apply(w, s, value);
return true;
}
}
/// <summary>
/// Builtin Drawer for Integer Debug Items.
/// </summary>
[DebugUIDrawer(typeof(DebugUI.IntField))]
public sealed class DebugUIDrawerIntField : DebugUIDrawer
{
/// <summary>
/// OnGUI implementation for Integer DebugUIDrawer.
/// </summary>
/// <param name="widget">DebugUI Widget.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
/// <returns>The state of the widget.</returns>
public override bool OnGUI(DebugUI.Widget widget, DebugState state)
{
var w = Cast<DebugUI.IntField>(widget);
var s = Cast<DebugStateInt>(state);
EditorGUI.BeginChangeCheck();
var rect = PrepareControlRect();
int value = w.min != null && w.max != null
? EditorGUI.IntSlider(rect, EditorGUIUtility.TrTextContent(w.displayName), w.GetValue(), w.min(), w.max())
: EditorGUI.IntField(rect, EditorGUIUtility.TrTextContent(w.displayName), w.GetValue());
if (EditorGUI.EndChangeCheck())
Apply(w, s, value);
return true;
}
}
/// <summary>
/// Builtin Drawer for Unsigned Integer Debug Items.
/// </summary>
[DebugUIDrawer(typeof(DebugUI.UIntField))]
public sealed class DebugUIDrawerUIntField : DebugUIDrawer
{
/// <summary>
/// OnGUI implementation for Unsigned Integer DebugUIDrawer.
/// </summary>
/// <param name="widget">DebugUI Widget.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
/// <returns>The state of the widget.</returns>
public override bool OnGUI(DebugUI.Widget widget, DebugState state)
{
var w = Cast<DebugUI.UIntField>(widget);
var s = Cast<DebugStateUInt>(state);
EditorGUI.BeginChangeCheck();
// No UIntField so we need to max to 0 ourselves or the value will wrap around
var rect = PrepareControlRect();
int tmp = w.min != null && w.max != null
? EditorGUI.IntSlider(rect, EditorGUIUtility.TrTextContent(w.displayName), Mathf.Max(0, (int)w.GetValue()), Mathf.Max(0, (int)w.min()), Mathf.Max(0, (int)w.max()))
: EditorGUI.IntField(rect, EditorGUIUtility.TrTextContent(w.displayName), Mathf.Max(0, (int)w.GetValue()));
uint value = (uint)Mathf.Max(0, tmp);
if (EditorGUI.EndChangeCheck())
Apply(w, s, value);
return true;
}
}
/// <summary>
/// Builtin Drawer for Float Debug Items.
/// </summary>
[DebugUIDrawer(typeof(DebugUI.FloatField))]
public sealed class DebugUIDrawerFloatField : DebugUIDrawer
{
/// <summary>
/// OnGUI implementation for Float DebugUIDrawer.
/// </summary>
/// <param name="widget">DebugUI Widget.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
/// <returns>The state of the widget.</returns>
public override bool OnGUI(DebugUI.Widget widget, DebugState state)
{
var w = Cast<DebugUI.FloatField>(widget);
var s = Cast<DebugStateFloat>(state);
EditorGUI.BeginChangeCheck();
var rect = PrepareControlRect();
float value = w.min != null && w.max != null
? EditorGUI.Slider(rect, EditorGUIUtility.TrTextContent(w.displayName), w.GetValue(), w.min(), w.max())
: EditorGUI.FloatField(rect, EditorGUIUtility.TrTextContent(w.displayName), w.GetValue());
if (EditorGUI.EndChangeCheck())
Apply(w, s, value);
return true;
}
}
/// <summary>
/// Builtin Drawer for Enum Debug Items.
/// </summary>
[DebugUIDrawer(typeof(DebugUI.EnumField))]
public sealed class DebugUIDrawerEnumField : DebugUIDrawer
{
/// <summary>
/// OnGUI implementation for Enum DebugUIDrawer.
/// </summary>
/// <param name="widget">DebugUI Widget.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
/// <returns>The state of the widget.</returns>
public override bool OnGUI(DebugUI.Widget widget, DebugState state)
{
var w = Cast<DebugUI.EnumField>(widget);
var s = Cast<DebugStateInt>(state);
if (w.indexes == null)
w.InitIndexes();
EditorGUI.BeginChangeCheck();
int index = -1;
int value = w.GetValue();
if (w.enumNames == null || w.enumValues == null)
{
EditorGUILayout.LabelField("Can't draw an empty enumeration.");
}
else
{
var rect = PrepareControlRect();
index = w.currentIndex;
// Fallback just in case, we may be handling sub/sectionned enums here
if (index < 0)
index = 0;
index = EditorGUI.IntPopup(rect, EditorGUIUtility.TrTextContent(w.displayName), index, w.enumNames, w.indexes);
value = w.enumValues[index];
}
if (EditorGUI.EndChangeCheck())
{
Apply(w, s, value);
if (index > -1)
w.currentIndex = index;
}
return true;
}
}
/// <summary>
/// Builtin Drawer for History Enum Debug Items.
/// </summary>
[DebugUIDrawer(typeof(DebugUI.HistoryEnumField))]
public sealed class DebugUIDrawerHistoryEnumField : DebugUIDrawer
{
/// <summary>
/// OnGUI implementation for History Enum DebugUIDrawer.
/// </summary>
/// <param name="widget">DebugUI Widget.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
/// <returns>The state of the widget.</returns>
public override bool OnGUI(DebugUI.Widget widget, DebugState state)
{
var w = Cast<DebugUI.HistoryEnumField>(widget);
var s = Cast<DebugStateInt>(state);
if (w.indexes == null)
w.InitIndexes();
EditorGUI.BeginChangeCheck();
int index = -1;
int value = w.GetValue();
if (w.enumNames == null || w.enumValues == null)
{
EditorGUILayout.LabelField("Can't draw an empty enumeration.");
}
else
{
var rect = PrepareControlRect();
index = w.currentIndex;
// Fallback just in case, we may be handling sub/sectionned enums here
if (index < 0)
index = 0;
var labelRect = rect;
labelRect.width = EditorGUIUtility.labelWidth;
const int oneValueWidth = 70;
var valueRects = new Rect[w.historyDepth + 1];
for (int i = 0; i < w.historyDepth + 1; i++)
{
valueRects[i] = rect;
valueRects[i].x += EditorGUIUtility.labelWidth + i * oneValueWidth;
valueRects[i].width = oneValueWidth;
}
EditorGUI.LabelField(labelRect, EditorGUIUtility.TrTextContent(w.displayName));
int indent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0; //be at left of rects
index = EditorGUI.IntPopup(valueRects[0], index, w.enumNames, w.indexes);
value = w.enumValues[index];
using (new EditorGUI.DisabledScope(true))
{
for (int i = 0; i < w.historyDepth; i++)
EditorGUI.IntPopup(valueRects[i + 1], w.GetHistoryValue(i), w.enumNames, w.indexes);
}
EditorGUI.indentLevel = indent;
}
if (EditorGUI.EndChangeCheck())
{
Apply(w, s, value);
if (index > -1)
w.currentIndex = index;
}
return true;
}
}
/// <summary>
/// Builtin Drawer for Bitfield Debug Items.
/// </summary>
[DebugUIDrawer(typeof(DebugUI.BitField))]
public sealed class DebugUIDrawerBitField : DebugUIDrawer
{
/// <summary>
/// OnGUI implementation for Bitfield DebugUIDrawer.
/// </summary>
/// <param name="widget">DebugUI Widget.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
/// <returns>The state of the widget.</returns>
public override bool OnGUI(DebugUI.Widget widget, DebugState state)
{
var w = Cast<DebugUI.BitField>(widget);
var s = Cast<DebugStateFlags>(state);
EditorGUI.BeginChangeCheck();
Enum value = w.GetValue();
var rect = PrepareControlRect();
// Skip first element (with value 0) because EditorGUI.MaskField adds a 'Nothing' field anyway
var enumNames = new string[w.enumNames.Length - 1];
for (int i = 0; i < enumNames.Length; i++)
enumNames[i] = w.enumNames[i + 1].text;
var index = EditorGUI.MaskField(rect, EditorGUIUtility.TrTextContent(w.displayName), (int)Convert.ToInt32(value), enumNames);
value = Enum.Parse(value.GetType(), index.ToString()) as Enum;
if (EditorGUI.EndChangeCheck())
Apply(w, s, value);
return true;
}
}
/// <summary>
/// Builtin Drawer for Foldout Debug Items.
/// </summary>
[DebugUIDrawer(typeof(DebugUI.Foldout))]
public sealed class DebugUIDrawerFoldout : DebugUIDrawer
{
/// <summary>
/// Begin implementation for Foldout DebugUIDrawer.
/// </summary>
/// <param name="widget">DebugUI Widget.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
public override void Begin(DebugUI.Widget widget, DebugState state)
{
var w = Cast<DebugUI.Foldout>(widget);
var s = Cast<DebugStateBool>(state);
EditorGUI.BeginChangeCheck();
Rect rect = PrepareControlRect();
bool value = EditorGUI.Foldout(rect, w.GetValue(), EditorGUIUtility.TrTextContent(w.displayName), true);
Rect drawRect = GUILayoutUtility.GetLastRect();
if (w.columnLabels != null && value)
{
const int oneColumnWidth = 70;
int indent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0; //be at left of rects
for (int i = 0; i < w.columnLabels.Length; i++)
{
var columnRect = drawRect;
columnRect.x += EditorGUIUtility.labelWidth + i * oneColumnWidth;
columnRect.width = oneColumnWidth;
EditorGUI.LabelField(columnRect, w.columnLabels[i] ?? "", EditorStyles.miniBoldLabel);
}
EditorGUI.indentLevel = indent;
}
if (EditorGUI.EndChangeCheck())
Apply(w, s, value);
EditorGUI.indentLevel++;
}
/// <summary>
/// OnGUI implementation for Foldout DebugUIDrawer.
/// </summary>
/// <param name="widget">DebugUI Widget.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
/// <returns>The state of the widget.</returns>
public override bool OnGUI(DebugUI.Widget widget, DebugState state)
{
var s = Cast<DebugStateBool>(state);
return s.value;
}
/// <summary>
/// End implementation for Foldout DebugUIDrawer.
/// </summary>
/// <param name="widget">DebugUI Widget.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
public override void End(DebugUI.Widget widget, DebugState state)
{
EditorGUI.indentLevel--;
}
}
/// <summary>
/// Builtin Drawer for Color Debug Items.
/// </summary>
[DebugUIDrawer(typeof(DebugUI.ColorField))]
public sealed class DebugUIDrawerColorField : DebugUIDrawer
{
/// <summary>
/// OnGUI implementation for Color DebugUIDrawer.
/// </summary>
/// <param name="widget">DebugUI Widget.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
/// <returns>The state of the widget.</returns>
public override bool OnGUI(DebugUI.Widget widget, DebugState state)
{
var w = Cast<DebugUI.ColorField>(widget);
var s = Cast<DebugStateColor>(state);
EditorGUI.BeginChangeCheck();
var rect = PrepareControlRect();
var value = EditorGUI.ColorField(rect, EditorGUIUtility.TrTextContent(w.displayName), w.GetValue(), w.showPicker, w.showAlpha, w.hdr);
if (EditorGUI.EndChangeCheck())
Apply(w, s, value);
return true;
}
}
/// <summary>
/// Builtin Drawer for Vector2 Debug Items.
/// </summary>
[DebugUIDrawer(typeof(DebugUI.Vector2Field))]
public sealed class DebugUIDrawerVector2Field : DebugUIDrawer
{
/// <summary>
/// OnGUI implementation for Vector2 DebugUIDrawer.
/// </summary>
/// <param name="widget">DebugUI Widget.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
/// <returns>The state of the widget.</returns>
public override bool OnGUI(DebugUI.Widget widget, DebugState state)
{
var w = Cast<DebugUI.Vector2Field>(widget);
var s = Cast<DebugStateVector2>(state);
EditorGUI.BeginChangeCheck();
var value = EditorGUILayout.Vector2Field(w.displayName, w.GetValue());
if (EditorGUI.EndChangeCheck())
Apply(w, s, value);
return true;
}
}
/// <summary>
/// Builtin Drawer for Vector3 Debug Items.
/// </summary>
[DebugUIDrawer(typeof(DebugUI.Vector3Field))]
public sealed class DebugUIDrawerVector3Field : DebugUIDrawer
{
/// <summary>
/// OnGUI implementation for Vector3 DebugUIDrawer.
/// </summary>
/// <param name="widget">DebugUI Widget.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
/// <returns>The state of the widget.</returns>
public override bool OnGUI(DebugUI.Widget widget, DebugState state)
{
var w = Cast<DebugUI.Vector3Field>(widget);
var s = Cast<DebugStateVector3>(state);
EditorGUI.BeginChangeCheck();
var value = EditorGUILayout.Vector3Field(w.displayName, w.GetValue());
if (EditorGUI.EndChangeCheck())
Apply(w, s, value);
return true;
}
}
/// <summary>
/// Builtin Drawer for Vector4 Debug Items.
/// </summary>
[DebugUIDrawer(typeof(DebugUI.Vector4Field))]
public sealed class DebugUIDrawerVector4Field : DebugUIDrawer
{
/// <summary>
/// OnGUI implementation for Vector4 DebugUIDrawer.
/// </summary>
/// <param name="widget">DebugUI Widget.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
/// <returns>The state of the widget.</returns>
public override bool OnGUI(DebugUI.Widget widget, DebugState state)
{
var w = Cast<DebugUI.Vector4Field>(widget);
var s = Cast<DebugStateVector4>(state);
EditorGUI.BeginChangeCheck();
var value = EditorGUILayout.Vector4Field(w.displayName, w.GetValue());
if (EditorGUI.EndChangeCheck())
Apply(w, s, value);
return true;
}
}
/// <summary>
/// Builtin Drawer for Container Debug Items.
/// </summary>
[DebugUIDrawer(typeof(DebugUI.Container))]
public sealed class DebugUIDrawerContainer : DebugUIDrawer
{
/// <summary>
/// Begin implementation for Container DebugUIDrawer.
/// </summary>
/// <param name="widget">DebugUI Widget.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
public override void Begin(DebugUI.Widget widget, DebugState state)
{
if (!string.IsNullOrEmpty(widget.displayName))
EditorGUILayout.LabelField(widget.displayName, EditorStyles.boldLabel);
EditorGUI.indentLevel++;
}
/// <summary>
/// End implementation for Container DebugUIDrawer.
/// </summary>
/// <param name="widget">DebugUI Widget.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
public override void End(DebugUI.Widget widget, DebugState state)
{
EditorGUI.indentLevel--;
}
}
/// <summary>
/// Builtin Drawer for Horizontal Box Debug Items.
/// </summary>
[DebugUIDrawer(typeof(DebugUI.HBox))]
public sealed class DebugUIDrawerHBox : DebugUIDrawer
{
/// <summary>
/// Begin implementation for Horizontal Box DebugUIDrawer.
/// </summary>
/// <param name="widget">DebugUI Widget.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
public override void Begin(DebugUI.Widget widget, DebugState state)
{
EditorGUILayout.BeginHorizontal();
}
/// <summary>
/// End implementation for Horizontal Box DebugUIDrawer.
/// </summary>
/// <param name="widget">DebugUI Widget.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
public override void End(DebugUI.Widget widget, DebugState state)
{
EditorGUILayout.EndHorizontal();
}
}
/// <summary>
/// Builtin Drawer for Vertical Box Debug Items.
/// </summary>
[DebugUIDrawer(typeof(DebugUI.VBox))]
public sealed class DebugUIDrawerVBox : DebugUIDrawer
{
/// <summary>
/// Begin implementation for Vertical Box DebugUIDrawer.
/// </summary>
/// <param name="widget">DebugUI Widget.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
public override void Begin(DebugUI.Widget widget, DebugState state)
{
EditorGUILayout.BeginVertical();
}
/// <summary>
/// End implementation for Vertical Box DebugUIDrawer.
/// </summary>
/// <param name="widget">DebugUI Widget.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
public override void End(DebugUI.Widget widget, DebugState state)
{
EditorGUILayout.EndVertical();
}
}
/// <summary>
/// Builtin Drawer for Table Debug Items.
/// </summary>
[DebugUIDrawer(typeof(DebugUI.Table))]
public sealed class DebugUIDrawerTable : DebugUIDrawer
{
/// <summary>
/// OnGUI implementation for Table DebugUIDrawer.
/// </summary>
/// <param name="widget">DebugUI Widget.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
/// <returns>The state of the widget.</returns>
public override bool OnGUI(DebugUI.Widget widget, DebugState state)
{
var w = Cast<DebugUI.Table>(widget);
var header = w.Header;
// Put some space before the array
PrepareControlRect(EditorGUIUtility.singleLineHeight * 0.5f);
// Draw an outline around the table
var rect = EditorGUI.IndentedRect(PrepareControlRect(header.height + (w.children.Count + 1) * EditorGUIUtility.singleLineHeight));
rect = DrawOutline(rect);
// Compute rects
var headerRect = new Rect(rect.x, rect.y, rect.width, header.height);
var contentRect = new Rect(rect.x, headerRect.yMax, rect.width, rect.height - headerRect.height);
var viewRect = new Rect(contentRect.x, contentRect.y, header.state.widthOfAllVisibleColumns, contentRect.height);
var rowRect = contentRect;
rowRect.height = EditorGUIUtility.singleLineHeight;
viewRect.height -= EditorGUIUtility.singleLineHeight;
// Show header
header.OnGUI(headerRect, Mathf.Max(w.scroll.x, 0f));
// Show array content
w.scroll = GUI.BeginScrollView(contentRect, w.scroll, viewRect);
{
var columns = header.state.columns;
var visible = header.state.visibleColumns;
for (int r = 0; r < w.children.Count; r++)
{
var row = Cast<DebugUI.Container>(w.children[r]);
rowRect.x = contentRect.x;
rowRect.width = columns[0].width;
rowRect.xMin += 2;
rowRect.xMax -= 2;
EditorGUI.LabelField(rowRect, GUIContent.none, EditorGUIUtility.TrTextContent(row.displayName));
rowRect.xMin -= 2;
rowRect.xMax += 2;
for (int c = 1; c < visible.Length; c++)
{
rowRect.x += rowRect.width;
rowRect.width = columns[visible[c]].width;
DisplayChild(rowRect, row.children[visible[c] - 1], w.isReadOnly);
}
rowRect.y += rowRect.height;
}
}
GUI.EndScrollView(false);
return false;
}
internal Rect DrawOutline(Rect rect)
{
if (Event.current.type != EventType.Repaint)
return rect;
float size = 1.0f;
var color = EditorGUIUtility.isProSkin ? new Color(0.12f, 0.12f, 0.12f, 1.333f) : new Color(0.6f, 0.6f, 0.6f, 1.333f);
Color orgColor = GUI.color;
GUI.color = GUI.color * color;
GUI.DrawTexture(new Rect(rect.x, rect.y, rect.width, size), EditorGUIUtility.whiteTexture);
GUI.DrawTexture(new Rect(rect.x, rect.yMax - size, rect.width, size), EditorGUIUtility.whiteTexture);
GUI.DrawTexture(new Rect(rect.x, rect.y + 1, size, rect.height - 2 * size), EditorGUIUtility.whiteTexture);
GUI.DrawTexture(new Rect(rect.xMax - size, rect.y + 1, size, rect.height - 2 * size), EditorGUIUtility.whiteTexture);
GUI.color = orgColor;
return new Rect(rect.x + size, rect.y + size, rect.width - 2 * size, rect.height - 2 * size);
}
internal void DisplayChild(Rect rect, DebugUI.Widget child, bool disable)
{
rect.xMin += 2;
rect.xMax -= 2;
if (child.GetType() == typeof(DebugUI.Value))
{
var widget = Cast<DebugUI.Value>(child);
EditorGUI.LabelField(rect, GUIContent.none, EditorGUIUtility.TrTextContent(widget.GetValue().ToString()));
}
else if (child.GetType() == typeof(DebugUI.ColorField))
{
var widget = Cast<DebugUI.ColorField>(child);
using (new EditorGUI.DisabledScope(disable))
EditorGUI.ColorField(rect, GUIContent.none, widget.GetValue(), false, widget.showAlpha, widget.hdr);
}
else if (child.GetType() == typeof(DebugUI.BoolField))
{
var widget = Cast<DebugUI.BoolField>(child);
using (new EditorGUI.DisabledScope(disable))
EditorGUI.Toggle(rect, GUIContent.none, widget.GetValue());
}
}
}
}

View File

@@ -0,0 +1,108 @@
using System;
using UnityEngine;
using UnityEngine.Rendering;
namespace UnityEditor.Rendering
{
/// <summary>
/// Attribute specifying wich type of Debug Item should this drawer be used with.
/// </summary>
public class DebugUIDrawerAttribute : Attribute
{
internal readonly Type type;
/// <summary>
/// Constructor for DebugUIDraw Attribute
/// </summary>
/// <param name="type">Type of Debug Item this draw should be used with.</param>
public DebugUIDrawerAttribute(Type type)
{
this.type = type;
}
}
/// <summary>
/// Debug Item Drawer
/// </summary>
public class DebugUIDrawer
{
/// <summary>
/// Cast into the proper type.
/// </summary>
/// <typeparam name="T">Type of the drawer</typeparam>
/// <param name="o">Object to be cast</param>
/// <returns>Returns o cast to type T</returns>
protected T Cast<T>(object o)
where T : class
{
var casted = o as T;
if (casted == null)
{
string typeName = o == null ? "null" : o.GetType().ToString();
throw new InvalidOperationException("Can't cast " + typeName + " to " + typeof(T));
}
return casted;
}
/// <summary>
/// Implement this to execute processing before UI rendering.
/// </summary>
/// <param name="widget">Widget that is going to be rendered.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
public virtual void Begin(DebugUI.Widget widget, DebugState state)
{}
/// <summary>
/// Implement this to execute UI rendering.
/// </summary>
/// <param name="widget">Widget that is going to be rendered.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
/// <returns>Returns the state of the widget.</returns>
public virtual bool OnGUI(DebugUI.Widget widget, DebugState state)
{
return true;
}
/// <summary>
/// Implement this to execute processing after UI rendering.
/// </summary>
/// <param name="widget">Widget that is going to be rendered.</param>
/// <param name="state">Debug State associated with the Debug Item.</param>
public virtual void End(DebugUI.Widget widget, DebugState state)
{}
/// <summary>
/// Applies a value to the widget and the Debug State of the Debug Item.
/// </summary>
/// <param name="widget">Debug Item widget.</param>
/// <param name="state">Debug State associated with the Debug Item</param>
/// <param name="value">Input value.</param>
protected void Apply(DebugUI.IValueField widget, DebugState state, object value)
{
Undo.RegisterCompleteObjectUndo(state, "Debug Property Change");
state.SetValue(value, widget);
widget.SetValue(value);
EditorUtility.SetDirty(state);
DebugState.m_CurrentDirtyState = state;
UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
}
/// <summary>
/// Prepares the rendering Rect of the Drawer/
/// </summary>
/// <param name="height">Height of the rect.</param>
/// <returns>Appropriate Rect for drawing.</returns>
protected Rect PrepareControlRect(float height = -1)
{
if (height < 0)
height = EditorGUIUtility.singleLineHeight;
var rect = GUILayoutUtility.GetRect(1f, 1f, height, height);
rect.width -= 2f;
rect.xMin += 2f;
EditorGUIUtility.labelWidth = rect.width / 2f;
return rect;
}
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Linq;
using UnityEditor;
using UnityEditor.Rendering;
using UnityEditorInternal;
using UnityEngine.Rendering;
namespace UnityEngine.Rendering.UI
{
[CustomEditor(typeof(DebugUIHandlerCanvas))]
sealed class DebugUIHandlerCanvasEditor : Editor
{
SerializedProperty m_PanelPrefab;
SerializedProperty m_Prefabs;
ReorderableList m_PrefabList;
static string[] s_Types; // Assembly qualified names
static string[] s_DisplayTypes; // Pretty names
static DebugUIHandlerCanvasEditor()
{
s_Types = CoreUtils.GetAllTypesDerivedFrom<DebugUI.Widget>()
.Where(t => !t.IsAbstract)
.Select(t => t.AssemblyQualifiedName)
.ToArray();
s_DisplayTypes = new string[s_Types.Length];
for (int i = 0; i < s_Types.Length; i++)
s_DisplayTypes[i] = Type.GetType(s_Types[i]).Name;
}
void OnEnable()
{
var o = new PropertyFetcher<DebugUIHandlerCanvas>(serializedObject);
m_PanelPrefab = o.Find(x => x.panelPrefab);
m_Prefabs = o.Find(x => x.prefabs);
m_PrefabList = new ReorderableList(serializedObject, m_Prefabs, true, true, true, true)
{
drawHeaderCallback = rect => EditorGUI.LabelField(rect, "Widget Prefabs"),
drawElementCallback = (rect, index, isActive, isFocused) =>
{
var element = m_PrefabList.serializedProperty.GetArrayElementAtIndex(index);
rect.y += 2f;
const float kTypeWidth = 100f;
// Type selector
var typeProp = element.FindPropertyRelative("type");
int typeIndex = ArrayUtility.IndexOf(s_Types, typeProp.stringValue);
typeIndex = Mathf.Max(typeIndex, 0);
typeIndex = EditorGUI.Popup(new Rect(rect.x, rect.y, kTypeWidth, EditorGUIUtility.singleLineHeight), typeIndex, s_DisplayTypes);
typeProp.stringValue = s_Types[typeIndex];
// Prefab
EditorGUI.PropertyField(
new Rect(rect.x + kTypeWidth + 2f, rect.y, rect.width - kTypeWidth - 2f, EditorGUIUtility.singleLineHeight),
element.FindPropertyRelative("prefab"), GUIContent.none);
},
onSelectCallback = list =>
{
var prefab = list.serializedProperty.GetArrayElementAtIndex(list.index).FindPropertyRelative("prefab").objectReferenceValue as GameObject;
if (prefab)
EditorGUIUtility.PingObject(prefab.gameObject);
}
};
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(m_PanelPrefab);
EditorGUILayout.Space();
m_PrefabList.DoLayoutList();
serializedObject.ApplyModifiedProperties();
}
}
}

View File

@@ -0,0 +1,584 @@
#if ENABLE_INPUT_SYSTEM && ENABLE_INPUT_SYSTEM_PACKAGE
#define USE_INPUT_SYSTEM
#endif
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.Callbacks;
using UnityEngine;
using UnityEngine.Rendering;
namespace UnityEditor.Rendering
{
#pragma warning disable 414
[Serializable]
sealed class WidgetStateDictionary : SerializedDictionary<string, DebugState> {}
sealed class DebugWindowSettings : ScriptableObject
{
// Keep these settings in a separate scriptable object so we can handle undo/redo on them
// without the rest of the debug window interfering
public int currentStateHash;
public int selectedPanel;
void OnEnable()
{
hideFlags = HideFlags.HideAndDontSave;
}
}
sealed class DebugWindow : EditorWindow
{
static readonly GUIContent k_ResetButtonContent = new GUIContent("Reset");
//static readonly GUIContent k_SaveButtonContent = new GUIContent("Save");
//static readonly GUIContent k_LoadButtonContent = new GUIContent("Load");
//static bool isMultiview = false;
static Styles s_Styles;
static GUIStyle s_SplitterLeft;
static float splitterPos = 150f;
const float minSideBarWidth = 100;
const float minContentWidth = 100;
bool dragging = false;
[SerializeField]
WidgetStateDictionary m_WidgetStates;
[SerializeField]
DebugWindowSettings m_Settings;
[SerializeField]
int m_DebugTreeState;
bool m_IsDirty;
Vector2 m_PanelScroll;
Vector2 m_ContentScroll;
static bool s_TypeMapDirty;
static Dictionary<Type, Type> s_WidgetStateMap; // DebugUI.Widget type -> DebugState type
static Dictionary<Type, DebugUIDrawer> s_WidgetDrawerMap; // DebugUI.Widget type -> DebugUIDrawer
static bool s_Open;
public static bool open
{
get => s_Open;
private set
{
if (s_Open ^ value)
OnDebugWindowToggled?.Invoke(value);
s_Open = value;
}
}
static event Action<bool> OnDebugWindowToggled;
[DidReloadScripts]
static void OnEditorReload()
{
s_TypeMapDirty = true;
//find if it where open, relink static event end propagate the info
open = (Resources.FindObjectsOfTypeAll<DebugWindow>()?.Length ?? 0) > 0;
if (OnDebugWindowToggled == null)
OnDebugWindowToggled += DebugManager.instance.ToggleEditorUI;
DebugManager.instance.ToggleEditorUI(open);
}
static void RebuildTypeMaps()
{
// Map states to widget (a single state can map to several widget types if the value to
// serialize is the same)
var attrType = typeof(DebugStateAttribute);
var stateTypes = CoreUtils.GetAllTypesDerivedFrom<DebugState>()
.Where(
t => t.IsDefined(attrType, false)
&& !t.IsAbstract
);
s_WidgetStateMap = new Dictionary<Type, Type>();
foreach (var stateType in stateTypes)
{
var attr = (DebugStateAttribute)stateType.GetCustomAttributes(attrType, false)[0];
foreach (var t in attr.types)
s_WidgetStateMap.Add(t, stateType);
}
// Drawers
attrType = typeof(DebugUIDrawerAttribute);
var types = CoreUtils.GetAllTypesDerivedFrom<DebugUIDrawer>()
.Where(
t => t.IsDefined(attrType, false)
&& !t.IsAbstract
);
s_WidgetDrawerMap = new Dictionary<Type, DebugUIDrawer>();
foreach (var t in types)
{
var attr = (DebugUIDrawerAttribute)t.GetCustomAttributes(attrType, false)[0];
var inst = (DebugUIDrawer)Activator.CreateInstance(t);
s_WidgetDrawerMap.Add(attr.type, inst);
}
// Done
s_TypeMapDirty = false;
}
[MenuItem("Window/Render Pipeline/Render Pipeline Debug", priority = 10005)] // 112 is hardcoded number given by the UxTeam to fit correctly in the Windows menu
static void Init()
{
var window = GetWindow<DebugWindow>();
window.titleContent = new GUIContent("Debug");
if (OnDebugWindowToggled == null)
OnDebugWindowToggled += DebugManager.instance.ToggleEditorUI;
open = true;
}
void OnEnable()
{
DebugManager.instance.refreshEditorRequested = false;
hideFlags = HideFlags.HideAndDontSave;
autoRepaintOnSceneChange = true;
if (m_Settings == null)
m_Settings = CreateInstance<DebugWindowSettings>();
// States are ScriptableObjects (necessary for Undo/Redo) but are not saved on disk so when the editor is closed then reopened, any existing debug window will have its states set to null
// Since we don't care about persistance in this case, we just re-init everything.
if (m_WidgetStates == null || !AreWidgetStatesValid())
m_WidgetStates = new WidgetStateDictionary();
if (s_WidgetStateMap == null || s_WidgetDrawerMap == null || s_TypeMapDirty)
RebuildTypeMaps();
Undo.undoRedoPerformed += OnUndoRedoPerformed;
DebugManager.instance.onSetDirty += MarkDirty;
// First init
m_DebugTreeState = DebugManager.instance.GetState();
UpdateWidgetStates();
EditorApplication.update -= Repaint;
var panels = DebugManager.instance.panels;
var selectedPanelIndex = m_Settings.selectedPanel;
if (selectedPanelIndex >= 0
&& selectedPanelIndex < panels.Count
&& panels[selectedPanelIndex].editorForceUpdate)
EditorApplication.update += Repaint;
}
// Note: this won't get called if the window is opened when the editor itself is closed
void OnDestroy()
{
open = false;
DebugManager.instance.onSetDirty -= MarkDirty;
Undo.ClearUndo(m_Settings);
DestroyWidgetStates();
}
public void DestroyWidgetStates()
{
if (m_WidgetStates != null)
{
// Clear all the states from memory
foreach (var state in m_WidgetStates)
{
var s = state.Value;
Undo.ClearUndo(s); // Don't leave dangling states in the global undo/redo stack
DestroyImmediate(s);
}
m_WidgetStates.Clear();
}
}
bool AreWidgetStatesValid()
{
foreach (var state in m_WidgetStates)
{
if (state.Value == null)
{
return false;
}
}
return true;
}
void MarkDirty()
{
m_IsDirty = true;
}
// We use item states to keep a cached value of each serializable debug items in order to
// handle domain reloads, play mode entering/exiting and undo/redo
// Note: no removal of orphan states
void UpdateWidgetStates()
{
foreach (var panel in DebugManager.instance.panels)
UpdateWidgetStates(panel);
}
void UpdateWidgetStates(DebugUI.IContainer container)
{
// Skip runtime only containers, we won't draw them so no need to serialize them either
var actualWidget = container as DebugUI.Widget;
if (actualWidget != null && actualWidget.isInactiveInEditor)
return;
// Recursively update widget states
foreach (var widget in container.children)
{
// Skip non-serializable widgets but still traverse them in case one of their
// children needs serialization support
var valueField = widget as DebugUI.IValueField;
if (valueField != null)
{
// Skip runtime & readonly only items
if (widget.isInactiveInEditor)
return;
var widgetType = widget.GetType();
string guid = widget.queryPath;
Type stateType;
s_WidgetStateMap.TryGetValue(widgetType, out stateType);
// Create missing states & recreate the ones that are null
if (stateType != null)
{
if (!m_WidgetStates.ContainsKey(guid) || m_WidgetStates[guid] == null)
{
var inst = (DebugState)CreateInstance(stateType);
inst.queryPath = guid;
inst.SetValue(valueField.GetValue(), valueField);
m_WidgetStates[guid] = inst;
}
}
}
// Recurse if the widget is a container
var containerField = widget as DebugUI.IContainer;
if (containerField != null)
UpdateWidgetStates(containerField);
}
}
public void ApplyStates(bool forceApplyAll = false)
{
if (!forceApplyAll && DebugState.m_CurrentDirtyState != null)
{
ApplyState(DebugState.m_CurrentDirtyState.queryPath, DebugState.m_CurrentDirtyState);
DebugState.m_CurrentDirtyState = null;
return;
}
foreach (var state in m_WidgetStates)
ApplyState(state.Key, state.Value);
DebugState.m_CurrentDirtyState = null;
}
void ApplyState(string queryPath, DebugState state)
{
var widget = DebugManager.instance.GetItem(queryPath) as DebugUI.IValueField;
if (widget == null)
return;
widget.SetValue(state.GetValue());
}
void OnUndoRedoPerformed()
{
int stateHash = ComputeStateHash();
// Something has been undone / redone, re-apply states to the debug tree
if (stateHash != m_Settings.currentStateHash)
{
ApplyStates(true);
m_Settings.currentStateHash = stateHash;
}
Repaint();
}
int ComputeStateHash()
{
unchecked
{
int hash = 13;
foreach (var state in m_WidgetStates)
hash = hash * 23 + state.Value.GetHashCode();
return hash;
}
}
void Update()
{
// If the render pipeline asset has been reloaded we force-refresh widget states in case
// some debug values need to be refresh/recreated as well (e.g. frame settings on HD)
if (DebugManager.instance.refreshEditorRequested)
{
DestroyWidgetStates();
DebugManager.instance.refreshEditorRequested = false;
}
int treeState = DebugManager.instance.GetState();
if (m_DebugTreeState != treeState || m_IsDirty)
{
UpdateWidgetStates();
ApplyStates();
m_DebugTreeState = treeState;
m_IsDirty = false;
}
}
void OnGUI()
{
if (s_Styles == null)
{
s_Styles = new Styles();
s_SplitterLeft = new GUIStyle();
}
var panels = DebugManager.instance.panels;
int itemCount = panels.Count(x => !x.isInactiveInEditor && x.children.Count(w => !w.isInactiveInEditor) > 0);
if (itemCount == 0)
{
EditorGUILayout.HelpBox("No debug item found.", MessageType.Info);
return;
}
// Background color
var wrect = position;
wrect.x = 0;
wrect.y = 0;
var oldColor = GUI.color;
GUI.color = s_Styles.skinBackgroundColor;
GUI.DrawTexture(wrect, EditorGUIUtility.whiteTexture);
GUI.color = oldColor;
GUILayout.BeginHorizontal(EditorStyles.toolbar);
//isMultiview = GUILayout.Toggle(isMultiview, "multiview", EditorStyles.toolbarButton);
//if (isMultiview)
// EditorGUILayout.Popup(0, new[] { new GUIContent("SceneView 1"), new GUIContent("SceneView 2") }, EditorStyles.toolbarDropDown, GUILayout.Width(100f));
GUILayout.FlexibleSpace();
//GUILayout.Button(k_LoadButtonContent, EditorStyles.toolbarButton);
//GUILayout.Button(k_SaveButtonContent, EditorStyles.toolbarButton);
if (GUILayout.Button(k_ResetButtonContent, EditorStyles.toolbarButton))
DebugManager.instance.Reset();
GUILayout.EndHorizontal();
using (new EditorGUILayout.HorizontalScope())
{
// Side bar
using (var scrollScope = new EditorGUILayout.ScrollViewScope(m_PanelScroll, s_Styles.sectionScrollView, GUILayout.Width(splitterPos)))
{
GUILayout.Space(40f);
if (m_Settings.selectedPanel >= panels.Count)
m_Settings.selectedPanel = 0;
// Validate container id
while (panels[m_Settings.selectedPanel].isInactiveInEditor || panels[m_Settings.selectedPanel].children.Count(x => !x.isInactiveInEditor) == 0)
{
m_Settings.selectedPanel++;
if (m_Settings.selectedPanel >= panels.Count)
m_Settings.selectedPanel = 0;
}
// Root children are containers
for (int i = 0; i < panels.Count; i++)
{
var panel = panels[i];
if (panel.isInactiveInEditor)
continue;
if (panel.children.Count(x => !x.isInactiveInEditor) == 0)
continue;
var elementRect = GUILayoutUtility.GetRect(EditorGUIUtility.TrTextContent(panel.displayName), s_Styles.sectionElement, GUILayout.ExpandWidth(true));
if (m_Settings.selectedPanel == i && Event.current.type == EventType.Repaint)
s_Styles.selected.Draw(elementRect, false, false, false, false);
EditorGUI.BeginChangeCheck();
GUI.Toggle(elementRect, m_Settings.selectedPanel == i, panel.displayName, s_Styles.sectionElement);
if (EditorGUI.EndChangeCheck())
{
Undo.RegisterCompleteObjectUndo(m_Settings, "Debug Panel Selection");
var previousPanel = m_Settings.selectedPanel >= 0 && m_Settings.selectedPanel < panels.Count
? panels[m_Settings.selectedPanel]
: null;
if (previousPanel != null && previousPanel.editorForceUpdate && !panel.editorForceUpdate)
EditorApplication.update -= Repaint;
else if ((previousPanel == null || !previousPanel.editorForceUpdate) && panel.editorForceUpdate)
EditorApplication.update += Repaint;
m_Settings.selectedPanel = i;
}
}
m_PanelScroll = scrollScope.scrollPosition;
}
Rect splitterRect = new Rect(splitterPos - 3, 0, 6, Screen.height);
GUI.Box(splitterRect, "", s_SplitterLeft);
GUILayout.Space(10f);
// Main section - traverse current container
using (var changedScope = new EditorGUI.ChangeCheckScope())
{
using (new EditorGUILayout.VerticalScope())
{
var selectedPanel = panels[m_Settings.selectedPanel];
GUILayout.Label(selectedPanel.displayName, s_Styles.sectionHeader);
GUILayout.Space(10f);
using (var scrollScope = new EditorGUILayout.ScrollViewScope(m_ContentScroll))
{
TraverseContainerGUI(selectedPanel);
m_ContentScroll = scrollScope.scrollPosition;
}
}
if (changedScope.changed)
{
m_Settings.currentStateHash = ComputeStateHash();
DebugManager.instance.ReDrawOnScreenDebug();
}
}
// Splitter events
if (Event.current != null)
{
switch (Event.current.rawType)
{
case EventType.MouseDown:
if (splitterRect.Contains(Event.current.mousePosition))
{
dragging = true;
}
break;
case EventType.MouseDrag:
if (dragging)
{
splitterPos += Event.current.delta.x;
splitterPos = Mathf.Clamp(splitterPos, minSideBarWidth, Screen.width - minContentWidth);
Repaint();
}
break;
case EventType.MouseUp:
if (dragging)
{
dragging = false;
}
break;
}
}
EditorGUIUtility.AddCursorRect(splitterRect, MouseCursor.ResizeHorizontal);
}
}
void OnWidgetGUI(DebugUI.Widget widget)
{
if (widget.isInactiveInEditor)
return;
DebugState state; // State will be null for stateless widget
m_WidgetStates.TryGetValue(widget.queryPath, out state);
DebugUIDrawer drawer;
if (!s_WidgetDrawerMap.TryGetValue(widget.GetType(), out drawer))
{
EditorGUILayout.LabelField("Drawer not found (" + widget.GetType() + ").");
}
else
{
drawer.Begin(widget, state);
if (drawer.OnGUI(widget, state))
{
var container = widget as DebugUI.IContainer;
if (container != null)
TraverseContainerGUI(container);
}
drawer.End(widget, state);
}
}
void TraverseContainerGUI(DebugUI.IContainer container)
{
// /!\ SHAAAAAAAME ALERT /!\
// A container can change at runtime because of the way IMGUI works and how we handle
// onValueChanged on widget so we have to take this into account while iterating
try
{
foreach (var widget in container.children)
OnWidgetGUI(widget);
}
catch (InvalidOperationException)
{
Repaint();
}
}
public class Styles
{
public static float s_DefaultLabelWidth = 0.5f;
public readonly GUIStyle sectionScrollView = "PreferencesSectionBox";
public readonly GUIStyle sectionElement = new GUIStyle("PreferencesSection");
public readonly GUIStyle selected = "OL SelectedRow";
public readonly GUIStyle sectionHeader = new GUIStyle(EditorStyles.largeLabel);
public readonly Color skinBackgroundColor;
public Styles()
{
sectionScrollView = new GUIStyle(sectionScrollView);
sectionScrollView.overflow.bottom += 1;
sectionElement.alignment = TextAnchor.MiddleLeft;
sectionHeader.fontStyle = FontStyle.Bold;
sectionHeader.fontSize = 18;
sectionHeader.margin.top = 10;
sectionHeader.margin.left += 1;
sectionHeader.normal.textColor = !EditorGUIUtility.isProSkin
? new Color(0.4f, 0.4f, 0.4f, 1.0f)
: new Color(0.7f, 0.7f, 0.7f, 1.0f);
if (EditorGUIUtility.isProSkin)
{
sectionHeader.normal.textColor = new Color(0.7f, 0.7f, 0.7f, 1.0f);
skinBackgroundColor = Color.gray * new Color(0.3f, 0.3f, 0.3f, 0.5f);
}
else
{
sectionHeader.normal.textColor = new Color(0.4f, 0.4f, 0.4f, 1.0f);
skinBackgroundColor = Color.gray * new Color(1f, 1f, 1f, 0.32f);
}
}
}
}
#pragma warning restore 414
}

View File

@@ -0,0 +1,34 @@
using UnityEngine.Rendering.UI;
namespace UnityEditor.Rendering.UI
{
[CustomEditor(typeof(UIFoldout), true)]
sealed class UIFoldoutEditor : Editor
{
SerializedProperty m_IsOn;
SerializedProperty m_Content;
SerializedProperty m_ArrowClosed;
SerializedProperty m_ArrowOpened;
void OnEnable()
{
var o = new PropertyFetcher<UIFoldout>(serializedObject);
m_IsOn = o.Find("m_IsOn");
m_Content = o.Find(x => x.content);
m_ArrowClosed = o.Find(x => x.arrowClosed);
m_ArrowOpened = o.Find(x => x.arrowOpened);
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(m_IsOn);
EditorGUILayout.PropertyField(m_Content);
EditorGUILayout.PropertyField(m_ArrowClosed);
EditorGUILayout.PropertyField(m_ArrowOpened);
serializedObject.ApplyModifiedProperties();
}
}
}

View File

@@ -0,0 +1,81 @@
using System;
namespace UnityEditor.Rendering
{
/// <summary>Bool flag saved in EditorPref</summary>
/// <typeparam name="T">Underlying enum type</typeparam>
public struct EditorPrefBoolFlags<T> : IEquatable<T>, IEquatable<EditorPrefBoolFlags<T>>
where T : struct, IConvertible
{
readonly string m_Key;
/// <summary>The value as the underlying enum type used</summary>
public T value
{ get => (T)(object)EditorPrefs.GetInt(m_Key); set => EditorPrefs.SetInt(m_Key, (int)(object)value); }
/// <summary>The raw value</summary>
public uint rawValue
{ get => (uint)EditorPrefs.GetInt(m_Key); set => EditorPrefs.SetInt(m_Key, (int)value); }
/// <summary>Constructor</summary>
/// <param name="key">Name of the Key in EditorPrefs to save the value</param>
public EditorPrefBoolFlags(string key) => m_Key = key;
/// <summary>Test if saved value is equal to the one given</summary>
/// <param name="other">Given value</param>
/// <returns>True if value are the same</returns>
public bool Equals(T other) => (int)(object)value == (int)(object)other;
/// <summary>Test if this EditorPrefBoolFlags is the same than the given one</summary>
/// <param name="other">Given EditorPrefBoolFlags</param>
/// <returns>True if they use the same value</returns>
public bool Equals(EditorPrefBoolFlags<T> other) => m_Key == other.m_Key;
/// <summary>Test if the given flags are set</summary>
/// <param name="v">Given flags</param>
/// <returns>True: all the given flags are set</returns>
public bool HasFlag(T v) => ((uint)(int)(object)v & rawValue) == (uint)(int)(object)v;
/// <summary>Set or unset the flags</summary>
/// <param name="f">Flags to edit</param>
/// <param name="v">Boolean value to set to the given flags</param>
public void SetFlag(T f, bool v)
{
if (v) rawValue |= (uint)(int)(object)f;
else rawValue &= ~(uint)(int)(object)f;
}
/// <summary>Explicit conversion operator to the underlying type</summary>
/// <param name="v">The EditorPrefBoolFlags to convert</param>
/// <returns>The converted value</returns>
public static explicit operator T(EditorPrefBoolFlags<T> v) => v.value;
/// <summary>Or operator between a EditorPrefBoolFlags and a value</summary>
/// <param name="l">The EditorPrefBoolFlags</param>
/// <param name="r">The value</param>
/// <returns>A EditorPrefBoolFlags with OR operator performed</returns>
public static EditorPrefBoolFlags<T> operator|(EditorPrefBoolFlags<T> l, T r)
{
l.rawValue |= (uint)(int)(object)r;
return l;
}
/// <summary>And operator between a EditorPrefBoolFlags and a value</summary>
/// <param name="l">The EditorPrefBoolFlags</param>
/// <param name="r">The value</param>
/// <returns>A EditorPrefBoolFlags with AND operator performed</returns>
public static EditorPrefBoolFlags<T> operator&(EditorPrefBoolFlags<T> l, T r)
{
l.rawValue &= (uint)(int)(object)r;
return l;
}
/// <summary>Xor operator between a EditorPrefBoolFlags and a value</summary>
/// <param name="l">The EditorPrefBoolFlags</param>
/// <param name="r">The value</param>
/// <returns>A EditorPrefBoolFlags with XOR operator performed</returns>
public static EditorPrefBoolFlags<T> operator^(EditorPrefBoolFlags<T> l, T r)
{
l.rawValue ^= (uint)(int)(object)r;
return l;
}
}
}

View File

@@ -0,0 +1,65 @@
using System;
namespace UnityEditor.Rendering
{
/// <summary>Used in editor drawer part to store the state of expendable areas.</summary>
/// <typeparam name="TState">An enum to use to describe the state.</typeparam>
/// <typeparam name="TTarget">A type given to automatically compute the key.</typeparam>
public struct ExpandedState<TState, TTarget>
where TState : struct, IConvertible
{
EditorPrefBoolFlags<TState> m_State;
/// <summary>Constructor will create the key to store in the EditorPref the state given generic type passed.</summary>
/// <param name="defaultValue">If key did not exist, it will be created with this value for initialization.</param>
/// <param name="prefix">[Optional] Prefix scope of the key (Default is CoreRP)</param>
public ExpandedState(TState defaultValue, string prefix = "CoreRP")
{
String Key = string.Format("{0}:{1}:UI_State", prefix, typeof(TTarget).Name);
m_State = new EditorPrefBoolFlags<TState>(Key);
//register key if not already there
if (!EditorPrefs.HasKey(Key))
{
EditorPrefs.SetInt(Key, (int)(object)defaultValue);
}
}
/// <summary>Get or set the state given the mask.</summary>
/// <param name="mask">The filtering mask</param>
/// <returns>True: All flagged area are expended</returns>
public bool this[TState mask]
{
get { return m_State.HasFlag(mask); }
set { m_State.SetFlag(mask, value); }
}
/// <summary>Accessor to the expended state of this specific mask.</summary>
/// <param name="mask">The filtering mask</param>
/// <returns>True: All flagged area are expended</returns>
public bool GetExpandedAreas(TState mask)
{
return m_State.HasFlag(mask);
}
/// <summary>Setter to the expended state.</summary>
/// <param name="mask">The filtering mask</param>
/// <param name="value">The expended state to set</param>
public void SetExpandedAreas(TState mask, bool value)
{
m_State.SetFlag(mask, value);
}
/// <summary> Utility to set all states to true </summary>
public void ExpandAll()
{
m_State.rawValue = ~(-1);
}
/// <summary> Utility to set all states to false </summary>
public void CollapseAll()
{
m_State.rawValue = 0;
}
}
}

View File

@@ -0,0 +1,782 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
namespace UnityEditor.Rendering
{
/// <summary>
/// An utility window used to list and filter a set of elements, as seen in the inspector when
/// clicking on the "Add Component" button.
/// </summary>
[InitializeOnLoad]
public class FilterWindow : EditorWindow
{
/// <summary>
/// The interface to implement to populate the list or tree and traverse its elements.
/// </summary>
public interface IProvider
{
/// <summary>
/// The position of the window on screen.
/// </summary>
Vector2 position { get; set; }
/// <summary>
/// Implement this method to populate the list or tree of elements.
/// </summary>
/// <param name="tree">The list to populate.</param>
void CreateComponentTree(List<Element> tree);
/// <summary>
/// Implement this method to define the behavior when an item is selected.
/// </summary>
/// <param name="element">The selected element.</param>
/// <param name="addIfComponent"></param>
/// <returns><c>true</c> if the window should close, <c>false</c> otherwise.</returns>
bool GoToChild(Element element, bool addIfComponent);
}
/// <summary>
/// The default width for the window.
/// </summary>
public static readonly float DefaultWidth = 250f;
/// <summary>
/// The default height for the window.
/// </summary>
public static readonly float DefaultHeight = 300f;
#region BaseElements
/// <summary>
/// An element from the filtered list or tree.
/// </summary>
/// <seealso cref="GroupElement"/>
public class Element : IComparable
{
/// <summary>
/// The current hierarchical level in the tree.
/// </summary>
public int level;
/// <summary>
/// The displayed content for the element.
/// </summary>
public GUIContent content;
/// <summary>
/// The name of the element as displayed in the UI.
/// </summary>
public string name
{
get { return content.text; }
}
/// <summary>
/// Compares this element to another object.
/// </summary>
/// <param name="o">The object to compare to.</param>
/// <returns><c>true</c> if both objects are the same, <c>false</c> otherwise.</returns>
public int CompareTo(object o)
{
return name.CompareTo((o as Element).name);
}
}
/// <summary>
/// A meta element used to group several elements in the list or tree.
/// </summary>
/// <seealso cref="Element"/>
[Serializable]
public class GroupElement : Element
{
/// <summary>
/// The current scroll position in the UI.
/// </summary>
public Vector2 scroll;
/// <summary>
/// The current selected index in the group.
/// </summary>
public int selectedIndex;
/// <summary>
/// Requests focus for the element.
/// </summary>
public bool WantsFocus { get; protected set; }
/// <summary>
/// Returns <c>true</c> if this group and its content should appear disabled in the UI.
/// </summary>
public virtual bool ShouldDisable
{
get { return false; }
}
/// <summary>
/// Creates a new <see cref="GroupElement"/>
/// </summary>
/// <param name="level">The group level.</param>
/// <param name="name">The display name for the group.</param>
public GroupElement(int level, string name)
{
this.level = level;
content = new GUIContent(name);
}
/// <summary>
/// Handles custom keyboard events on this group.
/// </summary>
/// <param name="evt">The event.</param>
/// <param name="window">A reference to the parent <see cref="FilterWindow"/>.</param>
/// <param name="goToParent">The action to execute if a "back" action is triggered in the UI.</param>
/// <returns><c>true</c> if the builtin events should execute for this group, <c>false</c> otherwise.</returns>
public virtual bool HandleKeyboard(Event evt, FilterWindow window, Action goToParent)
{
return false;
}
/// <summary>
/// A custom drawing method for this group.
/// </summary>
/// <param name="sFilterWindow">A reference to the parent <see cref="FilterWindow"/>.</param>
/// <returns><c>true</c> if the builtin drawing function should execute for this group,
/// <c>false</c> otherwise.</returns>
public virtual bool OnGUI(FilterWindow sFilterWindow)
{
return false;
}
}
#endregion
// Styles
class Styles
{
public GUIStyle header = (GUIStyle)typeof(EditorStyles).GetProperty("inspectorBig", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null, null);
public GUIStyle componentButton = new GUIStyle("PR Label");
public GUIStyle groupButton;
public GUIStyle background = "grey_border";
public GUIStyle rightArrow = "AC RightArrow";
public GUIStyle leftArrow = "AC LeftArrow";
public Styles()
{
header.font = EditorStyles.boldLabel.font;
componentButton.alignment = TextAnchor.MiddleLeft;
componentButton.fixedHeight = 20;
componentButton.imagePosition = ImagePosition.ImageAbove;
groupButton = new GUIStyle(componentButton);
}
}
const int k_HeaderHeight = 30;
const int k_WindowHeight = 400 - 80;
const int k_HelpHeight = 80 * 0;
const string k_ComponentSearch = "NodeSearchString";
static Styles s_Styles;
static FilterWindow s_FilterWindow;
static long s_LastClosedTime;
static bool s_DirtyList;
IProvider m_Provider;
Element[] m_Tree;
Element[] m_SearchResultTree;
List<GroupElement> m_Stack = new List<GroupElement>();
float m_Anim = 1;
int m_AnimTarget = 1;
long m_LastTime;
bool m_ScrollToSelected;
string m_DelayedSearch;
string m_Search = "";
bool m_HasSearch { get { return !string.IsNullOrEmpty(m_Search); } }
GroupElement m_ActiveParent { get { return m_Stack[m_Stack.Count - 2 + m_AnimTarget]; } }
Element[] m_ActiveTree { get { return m_HasSearch ? m_SearchResultTree : m_Tree; } }
Element m_ActiveElement
{
get
{
if (m_ActiveTree == null)
return null;
var children = GetChildren(m_ActiveTree, m_ActiveParent);
return children.Count == 0
? null
: children[m_ActiveParent.selectedIndex];
}
}
bool m_IsAnimating { get { return !Mathf.Approximately(m_Anim, m_AnimTarget); } }
static FilterWindow()
{
s_DirtyList = true;
}
void OnEnable()
{
s_FilterWindow = this;
m_Search = "";
}
void OnDisable()
{
s_LastClosedTime = DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond;
s_FilterWindow = null;
}
void OnLostFocus() => Close();
internal static bool ValidateAddComponentMenuItem()
{
return true;
}
internal static bool Show(Vector2 position, IProvider provider)
{
// If the window is already open, close it instead.
var wins = Resources.FindObjectsOfTypeAll(typeof(FilterWindow));
if (wins.Length > 0)
{
try
{
((EditorWindow)wins[0]).Close();
return false;
}
catch (Exception)
{
s_FilterWindow = null;
}
}
// We could not use realtimeSinceStartUp since it is set to 0 when entering/exitting
// playmode, we assume an increasing time when comparing time.
long nowMilliSeconds = DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond;
bool justClosed = nowMilliSeconds < s_LastClosedTime + 50;
if (!justClosed)
{
Event.current.Use();
if (s_FilterWindow == null)
{
s_FilterWindow = CreateInstance<FilterWindow>();
s_FilterWindow.hideFlags = HideFlags.HideAndDontSave;
}
s_FilterWindow.Init(position, provider);
return true;
}
return false;
}
static object Invoke(Type t, object inst, string method, params object[] args)
{
var flags = (inst == null ? BindingFlags.Static : BindingFlags.Instance) | BindingFlags.NonPublic;
var mi = t.GetMethod(method, flags);
return mi.Invoke(inst, args);
}
void Init(Vector2 position, IProvider provider)
{
m_Provider = provider;
// Has to be done before calling Show / ShowWithMode
m_Provider.position = position;
position = GUIUtility.GUIToScreenPoint(position);
var buttonRect = new Rect(position.x - DefaultWidth / 2, position.y - 16, DefaultWidth, 1);
CreateComponentTree();
ShowAsDropDown(buttonRect, new Vector2(buttonRect.width, k_WindowHeight));
Focus();
wantsMouseMove = true;
}
void CreateComponentTree()
{
var tree = new List<Element>();
m_Provider.CreateComponentTree(tree);
m_Tree = tree.ToArray();
// Rebuild stack
if (m_Stack.Count == 0)
{
m_Stack.Add(m_Tree[0] as GroupElement);
}
else
{
// The root is always the match for level 0
var match = m_Tree[0] as GroupElement;
int level = 0;
while (true)
{
// Assign the match for the current level
var oldElement = m_Stack[level];
m_Stack[level] = match;
m_Stack[level].selectedIndex = oldElement.selectedIndex;
m_Stack[level].scroll = oldElement.scroll;
// See if we reached last element of stack
level++;
if (level == m_Stack.Count)
break;
// Try to find a child of the same name as we had before
var children = GetChildren(m_ActiveTree, match);
var childMatch = children.FirstOrDefault(c => c.name == m_Stack[level].name);
if (childMatch is GroupElement)
{
match = childMatch as GroupElement;
}
else
{
// If we couldn't find the child, remove all further elements from the stack
while (m_Stack.Count > level)
m_Stack.RemoveAt(level);
}
}
}
s_DirtyList = false;
RebuildSearch();
}
internal void OnGUI()
{
// Avoids errors in the console if a domain reload is triggered while the filter window
// is opened
if (m_Provider == null)
return;
if (s_Styles == null)
s_Styles = new Styles();
GUI.Label(new Rect(0, 0, position.width, position.height), GUIContent.none, s_Styles.background);
if (s_DirtyList)
CreateComponentTree();
// Keyboard
HandleKeyboard();
GUILayout.Space(7);
// Search
if (!m_ActiveParent.WantsFocus)
{
EditorGUI.FocusTextInControl("ComponentSearch");
}
var searchRect = GUILayoutUtility.GetRect(10, 20);
searchRect.x += 8;
searchRect.width -= 16;
GUI.SetNextControlName("ComponentSearch");
using (new EditorGUI.DisabledScope(m_ActiveParent.ShouldDisable))
{
string newSearch = (string)Invoke(typeof(EditorGUI), null, "SearchField", searchRect, m_DelayedSearch ?? m_Search);
if (newSearch != m_Search || m_DelayedSearch != null)
{
if (!m_IsAnimating)
{
m_Search = m_DelayedSearch ?? newSearch;
EditorPrefs.SetString(k_ComponentSearch, m_Search);
RebuildSearch();
m_DelayedSearch = null;
}
else
{
m_DelayedSearch = newSearch;
}
}
}
// Show lists
ListGUI(m_ActiveTree, m_Anim, GetElementRelative(0), GetElementRelative(-1));
if (m_Anim < 1)
ListGUI(m_ActiveTree, m_Anim + 1, GetElementRelative(-1), GetElementRelative(-2));
// Animate
if (m_IsAnimating && Event.current.type == EventType.Repaint)
{
long now = DateTime.Now.Ticks;
float deltaTime = (now - m_LastTime) / (float)TimeSpan.TicksPerSecond;
m_LastTime = now;
m_Anim = Mathf.MoveTowards(m_Anim, m_AnimTarget, deltaTime * 4);
if (m_AnimTarget == 0 && Mathf.Approximately(m_Anim, 0))
{
m_Anim = 1;
m_AnimTarget = 1;
m_Stack.RemoveAt(m_Stack.Count - 1);
}
Repaint();
}
}
void HandleKeyboard()
{
var evt = Event.current;
if (evt.type == EventType.KeyDown)
{
// Special handling when in new script panel
if (!m_ActiveParent.HandleKeyboard(evt, s_FilterWindow, GoToParent))
{
// Always do these
if (evt.keyCode == KeyCode.DownArrow)
{
m_ActiveParent.selectedIndex++;
m_ActiveParent.selectedIndex = Mathf.Min(m_ActiveParent.selectedIndex, GetChildren(m_ActiveTree, m_ActiveParent).Count - 1);
m_ScrollToSelected = true;
evt.Use();
}
if (evt.keyCode == KeyCode.UpArrow)
{
m_ActiveParent.selectedIndex--;
m_ActiveParent.selectedIndex = Mathf.Max(m_ActiveParent.selectedIndex, 0);
m_ScrollToSelected = true;
evt.Use();
}
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter)
{
if (m_ActiveElement != null)
{
GoToChild(m_ActiveElement, true);
evt.Use();
}
}
// Do these if we're not in search mode
if (!m_HasSearch)
{
if (evt.keyCode == KeyCode.LeftArrow || evt.keyCode == KeyCode.Backspace)
{
GoToParent();
evt.Use();
}
if (evt.keyCode == KeyCode.RightArrow)
{
GoToChild(m_ActiveElement, false);
evt.Use();
}
if (evt.keyCode == KeyCode.Escape)
{
Close();
evt.Use();
}
}
}
}
}
const string k_SearchHeader = "Search";
void RebuildSearch()
{
if (!m_HasSearch)
{
m_SearchResultTree = null;
if (m_Stack[m_Stack.Count - 1].name == k_SearchHeader)
{
m_Stack.Clear();
m_Stack.Add(m_Tree[0] as GroupElement);
}
m_AnimTarget = 1;
m_LastTime = DateTime.Now.Ticks;
return;
}
// Support multiple search words separated by spaces.
var searchWords = m_Search.ToLower().Split(' ');
// We keep two lists. Matches that matches the start of an item always get first priority.
var matchesStart = new List<Element>();
var matchesWithin = new List<Element>();
foreach (var e in m_Tree)
{
if (e is GroupElement)
continue;
string name = e.name.ToLower().Replace(" ", "");
bool didMatchAll = true;
bool didMatchStart = false;
// See if we match ALL the seaarch words.
for (int w = 0; w < searchWords.Length; w++)
{
string search = searchWords[w];
if (name.Contains(search))
{
// If the start of the item matches the first search word, make a note of that.
if (w == 0 && name.StartsWith(search))
didMatchStart = true;
}
else
{
// As soon as any word is not matched, we disregard this item.
didMatchAll = false;
break;
}
}
// We always need to match all search words.
// If we ALSO matched the start, this item gets priority.
if (didMatchAll)
{
if (didMatchStart)
matchesStart.Add(e);
else
matchesWithin.Add(e);
}
}
matchesStart.Sort();
matchesWithin.Sort();
// Create search tree
var tree = new List<Element>();
// Add parent
tree.Add(new GroupElement(0, k_SearchHeader));
// Add search results
tree.AddRange(matchesStart);
tree.AddRange(matchesWithin);
// Create search result tree
m_SearchResultTree = tree.ToArray();
m_Stack.Clear();
m_Stack.Add(m_SearchResultTree[0] as GroupElement);
// Always select the first search result when search is changed (e.g. a character was typed in or deleted),
// because it's usually the best match.
if (GetChildren(m_ActiveTree, m_ActiveParent).Count >= 1)
m_ActiveParent.selectedIndex = 0;
else
m_ActiveParent.selectedIndex = -1;
}
GroupElement GetElementRelative(int rel)
{
int i = m_Stack.Count + rel - 1;
if (i < 0)
return null;
return m_Stack[i];
}
void GoToParent()
{
if (m_Stack.Count > 1)
{
m_AnimTarget = 0;
m_LastTime = DateTime.Now.Ticks;
}
}
void ListGUI(Element[] tree, float anim, GroupElement parent, GroupElement grandParent)
{
// Smooth the fractional part of the anim value
anim = Mathf.Floor(anim) + Mathf.SmoothStep(0, 1, Mathf.Repeat(anim, 1));
// Calculate rect for animated area
var animRect = position;
animRect.x = position.width * (1 - anim) + 1;
animRect.y = k_HeaderHeight;
animRect.height -= k_HeaderHeight + k_HelpHeight;
animRect.width -= 2;
// Start of animated area (the part that moves left and right)
GUILayout.BeginArea(animRect);
// Header
var headerRect = GUILayoutUtility.GetRect(10, 25);
string name = parent.name;
GUI.Label(headerRect, name, s_Styles.header);
// Back button
if (grandParent != null)
{
var arrowRect = new Rect(headerRect.x + 4, headerRect.y + 7, 13, 13);
var e = Event.current;
if (e.type == EventType.Repaint)
s_Styles.leftArrow.Draw(arrowRect, false, false, false, false);
if (e.type == EventType.MouseDown && headerRect.Contains(e.mousePosition))
{
GoToParent();
e.Use();
}
}
if (!parent.OnGUI(s_FilterWindow))
ListGUI(tree, parent);
GUILayout.EndArea();
}
void GoToChild(Element e, bool addIfComponent)
{
if (m_Provider.GoToChild(e, addIfComponent))
{
Close();
}
else if (!m_HasSearch)
{
m_LastTime = DateTime.Now.Ticks;
if (m_AnimTarget == 0)
{
m_AnimTarget = 1;
}
else if (Mathf.Approximately(m_Anim, 1f))
{
m_Anim = 0;
m_Stack.Add(e as GroupElement);
}
}
}
void ListGUI(Element[] tree, GroupElement parent)
{
// Start of scroll view list
parent.scroll = GUILayout.BeginScrollView(parent.scroll);
EditorGUIUtility.SetIconSize(new Vector2(16, 16));
var children = GetChildren(tree, parent);
var selectedRect = new Rect();
var evt = Event.current;
// Iterate through the children
for (int i = 0; i < children.Count; i++)
{
var e = children[i];
var r = GUILayoutUtility.GetRect(16, 20, GUILayout.ExpandWidth(true));
// Select the element the mouse cursor is over.
// Only do it on mouse move - keyboard controls are allowed to overwrite this until the next time the mouse moves.
if (evt.type == EventType.MouseMove || evt.type == EventType.MouseDown)
{
if (parent.selectedIndex != i && r.Contains(evt.mousePosition))
{
parent.selectedIndex = i;
Repaint();
}
}
bool selected = false;
// Handle selected item
if (i == parent.selectedIndex)
{
selected = true;
selectedRect = r;
}
// Draw element
if (evt.type == EventType.Repaint)
{
var labelStyle = (e is GroupElement) ? s_Styles.groupButton : s_Styles.componentButton;
labelStyle.Draw(r, e.content, false, false, selected, selected);
if (e is GroupElement)
{
var arrowRect = new Rect(r.x + r.width - 13, r.y + 4, 13, 13);
s_Styles.rightArrow.Draw(arrowRect, false, false, false, false);
}
}
if (evt.type == EventType.MouseDown && r.Contains(evt.mousePosition))
{
evt.Use();
parent.selectedIndex = i;
GoToChild(e, true);
}
}
EditorGUIUtility.SetIconSize(Vector2.zero);
GUILayout.EndScrollView();
// Scroll to show selected
if (m_ScrollToSelected && evt.type == EventType.Repaint)
{
m_ScrollToSelected = false;
var scrollRect = GUILayoutUtility.GetLastRect();
if (selectedRect.yMax - scrollRect.height > parent.scroll.y)
{
parent.scroll.y = selectedRect.yMax - scrollRect.height;
Repaint();
}
if (selectedRect.y < parent.scroll.y)
{
parent.scroll.y = selectedRect.y;
Repaint();
}
}
}
List<Element> GetChildren(Element[] tree, Element parent)
{
var children = new List<Element>();
int level = -1;
int i;
for (i = 0; i < tree.Length; i++)
{
if (tree[i] == parent)
{
level = parent.level + 1;
i++;
break;
}
}
if (level == -1)
return children;
for (; i < tree.Length; i++)
{
var e = tree[i];
if (e.level < level)
break;
if (e.level > level && !m_HasSearch)
continue;
children.Add(e);
}
return children;
}
}
}

View File

@@ -0,0 +1,26 @@
using System;
using UnityEngine;
namespace UnityEditor.Rendering
{
public static class GizmoUtility
{
public static Color GetHandleColor(Color baseColor)
{
baseColor.a = 1f;
return baseColor;
}
public static Color GetWireframeColor(Color baseColor)
{
baseColor.a = .7f;
return baseColor;
}
public static Color GetWireframeColorBehindObjects(Color baseColor)
{
baseColor.a = .2f;
return baseColor;
}
}
}

View File

@@ -0,0 +1,491 @@
using System;
using UnityEngine;
using System.Reflection;
namespace UnityEditor.Rendering
{
/// <summary>
/// Provide a gizmo/handle representing a box where all face can be moved independently.
/// Also add a contained sub gizmo/handle box if contained is used at creation.
/// </summary>
/// <example>
/// <code>
/// class MyComponentEditor : Editor
/// {
/// static HierarchicalBox box;
/// static HierarchicalBox containedBox;
///
/// static MyComponentEditor()
/// {
/// Color[] handleColors = new Color[]
/// {
/// Color.red,
/// Color.green,
/// Color.Blue,
/// new Color(0.5f, 0f, 0f, 1f),
/// new Color(0f, 0.5f, 0f, 1f),
/// new Color(0f, 0f, 0.5f, 1f)
/// };
/// box = new HierarchicalBox(new Color(1f, 1f, 1f, 0.25), handleColors);
/// containedBox = new HierarchicalBox(new Color(1f, 0f, 1f, 0.25), handleColors, container: box);
/// }
///
/// [DrawGizmo(GizmoType.Selected|GizmoType.Active)]
/// void DrawGizmo(MyComponent comp, GizmoType gizmoType)
/// {
/// box.center = comp.transform.position;
/// box.size = comp.transform.scale;
/// box.DrawHull(gizmoType == GizmoType.Selected);
///
/// containedBox.center = comp.innerposition;
/// containedBox.size = comp.innerScale;
/// containedBox.DrawHull(gizmoType == GizmoType.Selected);
/// }
///
/// void OnSceneGUI()
/// {
/// EditorGUI.BeginChangeCheck();
///
/// //container box must be also set for contained box for clamping
/// box.center = comp.transform.position;
/// box.size = comp.transform.scale;
/// box.DrawHandle();
///
/// containedBox.DrawHandle();
/// containedBox.center = comp.innerposition;
/// containedBox.size = comp.innerScale;
///
/// if(EditorGUI.EndChangeCheck())
/// {
/// comp.innerposition = containedBox.center;
/// comp.innersize = containedBox.size;
/// }
/// }
/// }
/// </code>
/// </example>
public class HierarchicalBox
{
const float k_HandleSizeCoef = 0.05f;
static Material k_Material_Cache;
static Material k_Material => (k_Material_Cache == null || k_Material_Cache.Equals(null) ? (k_Material_Cache = new Material(Shader.Find("Hidden/UnlitTransparentColored"))) : k_Material_Cache);
static Mesh k_MeshQuad_Cache;
static Mesh k_MeshQuad => k_MeshQuad_Cache == null || k_MeshQuad_Cache.Equals(null) ? (k_MeshQuad_Cache = Resources.GetBuiltinResource<Mesh>("Quad.fbx")) : k_MeshQuad_Cache;
enum NamedFace { Right, Top, Front, Left, Bottom, Back, None }
Material m_Material;
readonly Color[] m_PolychromeHandleColor;
readonly HierarchicalBox m_Parent;
Color m_MonochromeFillColor;
Color m_MonochromeHandleColor;
Color m_WireframeColor;
Color m_WireframeColorBehind;
int[] m_ControlIDs = new int[6] { 0, 0, 0, 0, 0, 0 };
bool m_MonoHandle = true;
Material material
{
get
{
if (m_Material == null || m_Material.Equals(null))
m_Material = new Material(k_Material);
//material can be lost when exiting play mode so gather the color again when reconstructing it
m_Material.color = m_MonochromeFillColor;
return m_Material;
}
}
/// <summary>
/// Allow to switch between the mode where all axis are controlled together or not
/// Note that if there is several handles, they will use the polychrome colors.
/// </summary>
public bool monoHandle { get => m_MonoHandle; set => m_MonoHandle = value; }
/// <summary>The position of the center of the box in Handle.matrix space.</summary>
public Vector3 center { get; set; }
/// <summary>The size of the box in Handle.matrix space.</summary>
public Vector3 size { get; set; }
/// <summary>The baseColor used to fill hull. All other colors are deduced from it except specific handle colors.</summary>
public Color baseColor
{
get { return material.color; }
set
{
value.a = 8f / 255;
SetBaseColor(value);
}
}
/// <summary>
/// Set the baseColor used to fill hull. All other colors are deduced from it except specific handle colors.
/// Instead of <see cref="baseColor">baseColor</see> set, this will not force the opacity and keep what is provided for the filled faces.
/// </summary>
/// <param name="color">The color to use</param>
public void SetBaseColor(Color color)
{
m_MonochromeFillColor = color;
material.color = m_MonochromeFillColor;
m_MonochromeHandleColor = GizmoUtility.GetHandleColor(color);
m_WireframeColor = GizmoUtility.GetWireframeColor(color);
m_WireframeColorBehind = GizmoUtility.GetWireframeColorBehindObjects(color);
}
//Note: Handles.Slider not allow to use a specific ControlID.
//Thus Slider1D is used (with reflection)
static Type k_Slider1D = Type.GetType("UnityEditorInternal.Slider1D, UnityEditor");
static MethodInfo k_Slider1D_Do = k_Slider1D
.GetMethod(
"Do",
BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public,
null,
CallingConventions.Any,
new[] { typeof(int), typeof(Vector3), typeof(Vector3), typeof(float), typeof(Handles.CapFunction), typeof(float) },
null);
static void Slider1D(int controlID, ref Vector3 handlePosition, Vector3 handleOrientation, float snapScale, Color color)
{
using (new Handles.DrawingScope(color))
{
handlePosition = (Vector3)k_Slider1D_Do.Invoke(null, new object[]
{
controlID,
handlePosition,
handleOrientation,
HandleUtility.GetHandleSize(handlePosition) * k_HandleSizeCoef,
new Handles.CapFunction(Handles.DotHandleCap),
snapScale
});
}
}
/// <summary>Constructor. Used to setup colors and also the container if any.</summary>
/// <param name="baseColor">The color of each face of the box. Other colors are deduced from it.</param>
/// <param name="polychromeHandleColors">The color of handle when they are separated. When they are grouped, they use a variation of the faceColor instead.</param>
/// <param name="parent">The HierarchicalBox containing this box. If null, the box will not be limited in size.</param>
public HierarchicalBox(Color baseColor, Color[] polychromeHandleColors = null, HierarchicalBox parent = null)
{
if (polychromeHandleColors != null && polychromeHandleColors.Length != 6)
throw new ArgumentException("polychromeHandleColors must be null or have a size of 6.");
m_Parent = parent;
m_Material = new Material(k_Material);
this.baseColor = baseColor;
m_PolychromeHandleColor = polychromeHandleColors ?? new Color[]
{
Handles.xAxisColor, Handles.yAxisColor, Handles.zAxisColor,
Handles.xAxisColor, Handles.yAxisColor, Handles.zAxisColor
};
}
/// <summary>Draw the hull which means the boxes without the handles</summary>
/// <param name="filled">If true, also fill the faces of the hull</param>
public void DrawHull(bool filled)
{
Color previousColor = Handles.color;
if (filled)
{
// Draw the hull
var xSize = new Vector3(size.z, size.y, 1f);
material.SetPass(0);
Graphics.DrawMeshNow(k_MeshQuad, Handles.matrix * Matrix4x4.TRS(center + size.x * .5f * Vector3.left, Quaternion.FromToRotation(Vector3.forward, Vector3.left), xSize));
Graphics.DrawMeshNow(k_MeshQuad, Handles.matrix * Matrix4x4.TRS(center + size.x * .5f * Vector3.right, Quaternion.FromToRotation(Vector3.forward, Vector3.right), xSize));
var ySize = new Vector3(size.x, size.z, 1f);
Graphics.DrawMeshNow(k_MeshQuad, Handles.matrix * Matrix4x4.TRS(center + size.y * .5f * Vector3.up, Quaternion.FromToRotation(Vector3.forward, Vector3.up), ySize));
Graphics.DrawMeshNow(k_MeshQuad, Handles.matrix * Matrix4x4.TRS(center + size.y * .5f * Vector3.down, Quaternion.FromToRotation(Vector3.forward, Vector3.down), ySize));
var zSize = new Vector3(size.x, size.y, 1f);
Graphics.DrawMeshNow(k_MeshQuad, Handles.matrix * Matrix4x4.TRS(center + size.z * .5f * Vector3.forward, Quaternion.identity, zSize));
Graphics.DrawMeshNow(k_MeshQuad, Handles.matrix * Matrix4x4.TRS(center + size.z * .5f * Vector3.back, Quaternion.FromToRotation(Vector3.forward, Vector3.back), zSize));
//if as a parent, also draw handle distance to the parent
if (m_Parent != null)
{
var centerDiff = center - m_Parent.center;
var xRecal = centerDiff;
var yRecal = centerDiff;
var zRecal = centerDiff;
xRecal.x = 0;
yRecal.y = 0;
zRecal.z = 0;
Handles.color = GetHandleColor(NamedFace.Left);
Handles.DrawLine(m_Parent.center + xRecal + m_Parent.size.x * .5f * Vector3.left, center + size.x * .5f * Vector3.left);
Handles.color = GetHandleColor(NamedFace.Right);
Handles.DrawLine(m_Parent.center + xRecal + m_Parent.size.x * .5f * Vector3.right, center + size.x * .5f * Vector3.right);
Handles.color = GetHandleColor(NamedFace.Top);
Handles.DrawLine(m_Parent.center + yRecal + m_Parent.size.y * .5f * Vector3.up, center + size.y * .5f * Vector3.up);
Handles.color = GetHandleColor(NamedFace.Bottom);
Handles.DrawLine(m_Parent.center + yRecal + m_Parent.size.y * .5f * Vector3.down, center + size.y * .5f * Vector3.down);
Handles.color = GetHandleColor(NamedFace.Front);
Handles.DrawLine(m_Parent.center + zRecal + m_Parent.size.z * .5f * Vector3.forward, center + size.z * .5f * Vector3.forward);
Handles.color = GetHandleColor(NamedFace.Back);
Handles.DrawLine(m_Parent.center + zRecal + m_Parent.size.z * .5f * Vector3.back, center + size.z * .5f * Vector3.back);
}
}
Handles.color = m_WireframeColor;
Handles.zTest = UnityEngine.Rendering.CompareFunction.LessEqual;
Handles.DrawWireCube(center, size);
Handles.color = m_WireframeColorBehind;
Handles.zTest = UnityEngine.Rendering.CompareFunction.Greater;
Handles.DrawWireCube(center, size);
Handles.zTest = UnityEngine.Rendering.CompareFunction.Always;
Handles.color = previousColor;
}
/// <summary>Draw the manipulable handles</summary>
public void DrawHandle()
{
Event evt = Event.current;
bool useHomothety = evt.shift;
bool useSymetry = evt.alt || evt.command;
// Note: snapping is handled natively on ctrl for each Slider1D
for (int i = 0, count = m_ControlIDs.Length; i < count; ++i)
m_ControlIDs[i] = GUIUtility.GetControlID("HierarchicalBox".GetHashCode() + i, FocusType.Passive);
var leftPosition = center + size.x * .5f * Vector3.left;
var rightPosition = center + size.x * .5f * Vector3.right;
var topPosition = center + size.y * .5f * Vector3.up;
var bottomPosition = center + size.y * .5f * Vector3.down;
var frontPosition = center + size.z * .5f * Vector3.forward;
var backPosition = center + size.z * .5f * Vector3.back;
var theChangedFace = NamedFace.None;
EditorGUI.BeginChangeCheck();
EditorGUI.BeginChangeCheck();
Slider1D(m_ControlIDs[(int)NamedFace.Left], ref leftPosition, Vector3.left, EditorSnapSettings.scale, GetHandleColor(NamedFace.Left));
if (EditorGUI.EndChangeCheck())
theChangedFace = NamedFace.Left;
EditorGUI.BeginChangeCheck();
Slider1D(m_ControlIDs[(int)NamedFace.Right], ref rightPosition, Vector3.right, EditorSnapSettings.scale, GetHandleColor(NamedFace.Right));
if (EditorGUI.EndChangeCheck())
theChangedFace = NamedFace.Right;
EditorGUI.BeginChangeCheck();
Slider1D(m_ControlIDs[(int)NamedFace.Top], ref topPosition, Vector3.up, EditorSnapSettings.scale, GetHandleColor(NamedFace.Top));
if (EditorGUI.EndChangeCheck())
theChangedFace = NamedFace.Top;
EditorGUI.BeginChangeCheck();
Slider1D(m_ControlIDs[(int)NamedFace.Bottom], ref bottomPosition, Vector3.down, EditorSnapSettings.scale, GetHandleColor(NamedFace.Bottom));
if (EditorGUI.EndChangeCheck())
theChangedFace = NamedFace.Bottom;
EditorGUI.BeginChangeCheck();
Slider1D(m_ControlIDs[(int)NamedFace.Front], ref frontPosition, Vector3.forward, EditorSnapSettings.scale, GetHandleColor(NamedFace.Front));
if (EditorGUI.EndChangeCheck())
theChangedFace = NamedFace.Front;
EditorGUI.BeginChangeCheck();
Slider1D(m_ControlIDs[(int)NamedFace.Back], ref backPosition, Vector3.back, EditorSnapSettings.scale, GetHandleColor(NamedFace.Back));
if (EditorGUI.EndChangeCheck())
theChangedFace = NamedFace.Back;
if (EditorGUI.EndChangeCheck())
{
float delta = 0f;
switch (theChangedFace)
{
case NamedFace.Left: delta = (leftPosition - center - size.x * .5f * Vector3.left).x; break;
case NamedFace.Right: delta = -(rightPosition - center - size.x * .5f * Vector3.right).x; break;
case NamedFace.Top: delta = -(topPosition - center - size.y * .5f * Vector3.up).y; break;
case NamedFace.Bottom: delta = (bottomPosition - center - size.y * .5f * Vector3.down).y; break;
case NamedFace.Front: delta = -(frontPosition - center - size.z * .5f * Vector3.forward).z; break;
case NamedFace.Back: delta = (backPosition - center - size.z * .5f * Vector3.back).z; break;
}
if (monoHandle || useHomothety && useSymetry)
{
var tempSize = size - Vector3.one * delta;
//ensure that the box face are still facing outside
for (int axis = 0; axis < 3; ++axis)
{
if (tempSize[axis] < 0)
{
delta += tempSize[axis];
tempSize = size - Vector3.one * delta;
}
}
//ensure containedBox do not exit container
if (m_Parent != null)
{
for (int axis = 0; axis < 3; ++axis)
{
if (tempSize[axis] > m_Parent.size[axis])
tempSize[axis] = m_Parent.size[axis];
}
}
size = tempSize;
}
else
{
if (useSymetry)
{
switch (theChangedFace)
{
case NamedFace.Left: rightPosition.x -= delta; break;
case NamedFace.Right: leftPosition.x += delta; break;
case NamedFace.Top: bottomPosition.y += delta; break;
case NamedFace.Bottom: topPosition.y -= delta; break;
case NamedFace.Front: backPosition.z += delta; break;
case NamedFace.Back: frontPosition.z -= delta; break;
}
//ensure that the box face are still facing outside
switch (theChangedFace)
{
case NamedFace.Left:
case NamedFace.Right:
if (rightPosition.x < leftPosition.x)
rightPosition.x = leftPosition.x = center.x;
break;
case NamedFace.Top:
case NamedFace.Bottom:
if (topPosition.y < bottomPosition.y)
topPosition.y = bottomPosition.y = center.y;
break;
case NamedFace.Front:
case NamedFace.Back:
if (frontPosition.z < backPosition.z)
frontPosition.z = backPosition.z = center.z;
break;
}
}
if (useHomothety)
{
float halfDelta = delta * 0.5f;
switch (theChangedFace)
{
case NamedFace.Left:
case NamedFace.Right:
bottomPosition.y += halfDelta;
topPosition.y -= halfDelta;
backPosition.z += halfDelta;
frontPosition.z -= halfDelta;
break;
case NamedFace.Top:
case NamedFace.Bottom:
rightPosition.x -= halfDelta;
leftPosition.x += halfDelta;
backPosition.z += halfDelta;
frontPosition.z -= halfDelta;
break;
case NamedFace.Front:
case NamedFace.Back:
rightPosition.x -= halfDelta;
leftPosition.x += halfDelta;
bottomPosition.y += halfDelta;
topPosition.y -= halfDelta;
break;
}
//ensure that the box face are still facing outside
switch (theChangedFace)
{
case NamedFace.Left:
if (rightPosition.x < leftPosition.x)
leftPosition.x = rightPosition.x;
if (topPosition.y < bottomPosition.y)
topPosition.y = bottomPosition.y = center.y;
if (frontPosition.z < backPosition.z)
frontPosition.z = backPosition.z = center.z;
break;
case NamedFace.Right:
if (rightPosition.x < leftPosition.x)
rightPosition.x = leftPosition.x;
if (topPosition.y < bottomPosition.y)
topPosition.y = bottomPosition.y = center.y;
if (frontPosition.z < backPosition.z)
frontPosition.z = backPosition.z = center.z;
break;
case NamedFace.Top:
if (topPosition.y < bottomPosition.y)
topPosition.y = bottomPosition.y;
if (rightPosition.x < leftPosition.x)
rightPosition.x = leftPosition.x = center.x;
if (frontPosition.z < backPosition.z)
frontPosition.z = backPosition.z = center.z;
break;
case NamedFace.Bottom:
if (topPosition.y < bottomPosition.y)
bottomPosition.y = topPosition.y;
if (rightPosition.x < leftPosition.x)
rightPosition.x = leftPosition.x = center.x;
if (frontPosition.z < backPosition.z)
frontPosition.z = backPosition.z = center.z;
break;
case NamedFace.Front:
if (frontPosition.z < backPosition.z)
frontPosition.z = backPosition.z;
if (rightPosition.x < leftPosition.x)
rightPosition.x = leftPosition.x = center.x;
if (topPosition.y < bottomPosition.y)
topPosition.y = bottomPosition.y = center.y;
break;
case NamedFace.Back:
if (frontPosition.z < backPosition.z)
backPosition.z = frontPosition.z;
if (rightPosition.x < leftPosition.x)
rightPosition.x = leftPosition.x = center.x;
if (topPosition.y < bottomPosition.y)
topPosition.y = bottomPosition.y = center.y;
break;
}
}
var max = new Vector3(rightPosition.x, topPosition.y, frontPosition.z);
var min = new Vector3(leftPosition.x, bottomPosition.y, backPosition.z);
if (!useSymetry && !useHomothety)
{
//ensure that the box face are still facing outside
for (int axis = 0; axis < 3; ++axis)
{
if (min[axis] > max[axis])
{
// Control IDs in m_ControlIDs[0-3[ are for positive axes
if (GUIUtility.hotControl == m_ControlIDs[axis])
max[axis] = min[axis];
else
min[axis] = max[axis];
}
}
}
//ensure containedBox do not exit container
if (m_Parent != null)
{
for (int axis = 0; axis < 3; ++axis)
{
if (min[axis] < m_Parent.center[axis] - m_Parent.size[axis] * 0.5f)
min[axis] = m_Parent.center[axis] - m_Parent.size[axis] * 0.5f;
if (max[axis] > m_Parent.center[axis] + m_Parent.size[axis] * 0.5f)
max[axis] = m_Parent.center[axis] + m_Parent.size[axis] * 0.5f;
}
}
center = (max + min) * .5f;
size = max - min;
}
}
}
Color GetHandleColor(NamedFace name) => monoHandle ? m_MonochromeHandleColor : m_PolychromeHandleColor[(int)name];
}
}

View File

@@ -0,0 +1,176 @@
using UnityEngine;
namespace UnityEditor.Rendering
{
/// <summary>
/// Provide a gizmo/handle representing a box where all face can be moved independently.
/// Also add a contained sub gizmo/handle box if contained is used at creation.
/// </summary>
/// <example>
/// <code>
/// class MyComponentEditor : Editor
/// {
/// static HierarchicalSphere sphere;
/// static HierarchicalSphere containedSphere;
///
/// static MyComponentEditor()
/// {
/// Color[] handleColors = new Color[]
/// {
/// Color.red,
/// Color.green,
/// Color.Blue,
/// new Color(0.5f, 0f, 0f, 1f),
/// new Color(0f, 0.5f, 0f, 1f),
/// new Color(0f, 0f, 0.5f, 1f)
/// };
/// sphere = new HierarchicalSphere(new Color(1f, 1f, 1f, 0.25));
/// containedSphere = new HierarchicalSphere(new Color(1f, 0f, 1f, 0.25), container: sphere);
/// }
///
/// [DrawGizmo(GizmoType.Selected|GizmoType.Active)]
/// void DrawGizmo(MyComponent comp, GizmoType gizmoType)
/// {
/// sphere.center = comp.transform.position;
/// sphere.size = comp.transform.scale;
/// sphere.DrawHull(gizmoType == GizmoType.Selected);
///
/// containedSphere.center = comp.innerposition;
/// containedSphere.size = comp.innerScale;
/// containedSphere.DrawHull(gizmoType == GizmoType.Selected);
/// }
///
/// void OnSceneGUI()
/// {
/// EditorGUI.BeginChangeCheck();
///
/// //container sphere must be also set for contained sphere for clamping
/// sphere.center = comp.transform.position;
/// sphere.size = comp.transform.scale;
/// sphere.DrawHandle();
///
/// containedSphere.center = comp.innerposition;
/// containedSphere.size = comp.innerScale;
/// containedSphere.DrawHandle();
///
/// if(EditorGUI.EndChangeCheck())
/// {
/// comp.innerposition = containedSphere.center;
/// comp.innersize = containedSphere.size;
/// }
/// }
/// }
/// </code>
/// </example>
public class HierarchicalSphere
{
const float k_HandleSizeCoef = 0.05f;
static Material k_Material_Cache;
static Material k_Material => (k_Material_Cache == null || k_Material_Cache.Equals(null) ? (k_Material_Cache = new Material(Shader.Find("Hidden/UnlitTransparentColored"))) : k_Material_Cache);
static Mesh k_MeshSphere_Cache;
static Mesh k_MeshSphere => k_MeshSphere_Cache == null || k_MeshSphere_Cache.Equals(null) ? (k_MeshSphere_Cache = Resources.GetBuiltinResource<Mesh>("New-Sphere.fbx")) : k_MeshSphere_Cache;
Material m_Material;
readonly HierarchicalSphere m_Parent;
Color m_HandleColor;
Color m_WireframeColor;
Color m_WireframeColorBehind;
Material material => m_Material == null || m_Material.Equals(null)
? (m_Material = new Material(k_Material))
: m_Material;
/// <summary>The position of the center of the box in Handle.matrix space.</summary>
public Vector3 center { get; set; }
/// <summary>The size of the box in Handle.matrix space.</summary>
public float radius { get; set; }
/// <summary>The baseColor used to fill hull. All other colors are deduced from it.</summary>
public Color baseColor
{
get { return material.color; }
set
{
value.a = 8f / 255;
material.color = value;
value.a = 1f;
m_HandleColor = value;
value.a = 0.7f;
m_WireframeColor = value;
value.a = 0.2f;
m_WireframeColorBehind = value;
}
}
/// <summary>Constructor. Used to setup colors and also the container if any.</summary>
/// <param name="baseColor">The color of filling. All other colors are deduced from it.</param>
/// <param name="parent">The HierarchicalSphere containing this sphere. If null, the sphere will not be limited in size.</param>
public HierarchicalSphere(Color baseColor, HierarchicalSphere parent = null)
{
m_Parent = parent;
m_Material = new Material(k_Material);
this.baseColor = baseColor;
}
/// <summary>Draw the hull which means the boxes without the handles</summary>
/// <param name="filled">If true, also draw the surface of the hull's sphere</param>
public void DrawHull(bool filled)
{
Color wireframeColor = m_HandleColor;
wireframeColor.a = 0.8f;
using (new Handles.DrawingScope(m_WireframeColor, Matrix4x4.TRS((Vector3)Handles.matrix.GetColumn(3) + center, Quaternion.identity, Vector3.one)))
{
if (filled)
{
material.SetPass(0);
Matrix4x4 drawMatrix = Matrix4x4.TRS((Vector3)Handles.matrix.GetColumn(3), Quaternion.identity, Vector3.one * radius * 2f);
Graphics.DrawMeshNow(k_MeshSphere, drawMatrix);
}
var drawCenter = Vector3.zero;
var viewPlaneNormal = Vector3.zero;
var drawnRadius = radius;
if (Camera.current.orthographic)
viewPlaneNormal = Camera.current.transform.forward;
else
{
viewPlaneNormal = (Vector3)Handles.matrix.GetColumn(3) - Camera.current.transform.position;
var sqrDist = viewPlaneNormal.sqrMagnitude; // squared distance from camera to center
var sqrRadius = radius * radius; // squared radius
var sqrOffset = sqrRadius * sqrRadius / sqrDist; // squared distance from actual center to drawn disc center
var insideAmount = sqrOffset / sqrRadius;
// If we are not inside the sphere, calculate where to draw the periphery
if (insideAmount < 1)
{
drawnRadius = Mathf.Sqrt(sqrRadius - sqrOffset); // the radius of the drawn disc
drawCenter -= (sqrRadius / sqrDist) * viewPlaneNormal;
}
}
Handles.zTest = UnityEngine.Rendering.CompareFunction.LessEqual;
Handles.DrawWireDisc(Vector3.zero, Vector3.up, radius);
Handles.DrawWireDisc(drawCenter, viewPlaneNormal, drawnRadius);
Handles.color = m_WireframeColorBehind;
Handles.zTest = UnityEngine.Rendering.CompareFunction.Greater;
Handles.DrawWireDisc(Vector3.zero, Vector3.up, radius);
Handles.DrawWireDisc(drawCenter, viewPlaneNormal, drawnRadius);
Handles.zTest = UnityEngine.Rendering.CompareFunction.Always;
}
}
/// <summary>Draw the manipulable handles</summary>
public void DrawHandle()
{
using (new Handles.DrawingScope(m_HandleColor))
{
radius = Handles.RadiusHandle(Quaternion.identity, center, radius, handlesOnly: true);
if (m_Parent != null)
radius = Mathf.Min(radius, m_Parent.radius - Vector3.Distance(center, m_Parent.center));
}
}
}
}

View File

@@ -0,0 +1,20 @@
Shader "Hidden/UnlitTransparentColored" {
Properties {
_Color ("Main Color", Color) = (1,1,1,1)
}
SubShader {
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
ZWrite Off
Lighting Off
Fog { Mode Off }
Cull Off
Blend SrcAlpha OneMinusSrcAlpha
Pass {
Color [_Color]
}
}
}

View File

@@ -0,0 +1,979 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace UnityEditor.Rendering
{
// A good chunk of this class is recycled from Post-processing v2 and could use a cleaning pass
/// <summary>
/// A custom curve editor made to be embedded in the inspector instead of a separate window.
/// </summary>
public sealed class InspectorCurveEditor
{
enum EditMode
{
None,
Moving,
TangentEdit
}
enum Tangent
{
In,
Out
}
/// <summary>
/// A structure holding settings used for the curve editor.
/// </summary>
public struct Settings
{
/// <summary>
/// The boundaries of the curve.
/// </summary>
public Rect bounds;
/// <summary>
/// The visual padding used when rendering the curve in the inspector.
/// </summary>
public RectOffset padding;
/// <summary>
/// The color to use when a curve is selected.
/// </summary>
public Color selectionColor;
/// <summary>
/// The distance in pixels to check for curve selection on mouse click.
/// </summary>
public float curvePickingDistance;
/// <summary>
/// The distance to clamp keys at compared to the previous and next keys.
/// </summary>
public float keyTimeClampingDistance;
/// <summary>
/// Default settings.
/// - <see cref="bounds"/> are set to (0, 0, 1, 1).
/// - <see cref="padding"/> is set to (0, 0, 0, 0).
/// - <see cref="selectionColor"/> is set to yellow.
/// - <see cref="curvePickingDistance"/> is set to 6 pixels.
/// - <see cref="keyTimeClampingDistance"/> is set to 1e-4.
/// </summary>
public static Settings defaultSettings => new Settings
{
bounds = new Rect(0f, 0f, 1f, 1f),
padding = new RectOffset(0, 0, 0, 0),
selectionColor = Color.yellow,
curvePickingDistance = 6f,
keyTimeClampingDistance = 1e-4f
};
}
/// <summary>
/// A structure holding the state of a single curve in the editor.
/// </summary>
public struct CurveState
{
/// <summary>
/// Is the curve visible?
/// </summary>
public bool visible;
/// <summary>
/// Is the curve editable?
/// </summary>
public bool editable;
/// <summary>
/// The minimum allowed number of points on the curve.
/// </summary>
public uint minPointCount;
/// <summary>
/// A constant value to use when the curve doesn't have any point.
/// </summary>
public float zeroKeyConstantValue;
/// <summary>
/// The color used to draw the curve.
/// </summary>
public Color color;
/// <summary>
/// The visual thickness of the curve.
/// </summary>
public float width;
/// <summary>
/// The visual thickness of the curve handles.
/// </summary>
public float handleWidth;
/// <summary>
/// Should the handles be visible on non-editable curves?
/// </summary>
public bool showNonEditableHandles;
/// <summary>
/// Should the handles only be visible when the curve is selected?
/// </summary>
public bool onlyShowHandlesOnSelection;
/// <summary>
/// Does this curve loop in the defined boudaries?
/// </summary>
public bool loopInBounds;
/// <summary>
/// Default curve state.
/// - <see cref="visible"/> is set to true.
/// - <see cref="editable"/> is set to true.
/// - <see cref="minPointCount"/> is set to 2.
/// - <see cref="zeroKeyConstantValue"/> is set to 0.
/// - <see cref="color"/> is set to white.
/// - <see cref="width"/> is set to 2.
/// - <see cref="handleWidth"/> is set to 2.
/// - <see cref="showNonEditableHandles"/> is set to true.
/// - <see cref="onlyShowHandlesOnSelection"/> is set to false.
/// - <see cref="loopInBounds"/> is set to false.
/// </summary>
public static CurveState defaultState => new CurveState
{
visible = true,
editable = true,
minPointCount = 2,
zeroKeyConstantValue = 0f,
color = Color.white,
width = 2f,
handleWidth = 2f,
showNonEditableHandles = true,
onlyShowHandlesOnSelection = false,
loopInBounds = false
};
}
/// <summary>
/// A structure holding the state of the current selection.
/// </summary>
public struct Selection
{
/// <summary>
/// A reference to the serialized <c>AnimationCurve</c>.
/// </summary>
public SerializedProperty curve;
/// <summary>
/// The currently selected key index, or -1 if none is selected.
/// </summary>
public int keyframeIndex;
/// <summary>
/// The key itself, or <c>null</c> if none is selected.
/// </summary>
public Keyframe? keyframe;
/// <summary>
/// Creates a new selection state.
/// </summary>
/// <param name="curve">A reference to the serialized curve.</param>
/// <param name="keyframeIndex">The currently selected key index, or -1 if none is selected.</param>
/// <param name="keyframe">The key itself, or <c>null</c> if none is selected.</param>
public Selection(SerializedProperty curve, int keyframeIndex, Keyframe? keyframe)
{
this.curve = curve;
this.keyframeIndex = keyframeIndex;
this.keyframe = keyframe;
}
}
internal struct MenuAction
{
internal SerializedProperty curve;
internal int index;
internal Vector3 position;
internal MenuAction(SerializedProperty curve)
{
this.curve = curve;
this.index = -1;
this.position = Vector3.zero;
}
internal MenuAction(SerializedProperty curve, int index)
{
this.curve = curve;
this.index = index;
this.position = Vector3.zero;
}
internal MenuAction(SerializedProperty curve, Vector3 position)
{
this.curve = curve;
this.index = -1;
this.position = position;
}
}
/// <summary>
/// The current settings used for the curve editor.
/// </summary>
public readonly Settings settings;
readonly Dictionary<SerializedProperty, CurveState> m_Curves;
Rect m_CurveArea;
SerializedProperty m_SelectedCurve;
int m_SelectedKeyframeIndex = -1;
EditMode m_EditMode = EditMode.None;
Tangent m_TangentEditMode;
bool m_Dirty;
/// <summary>
/// Creates a curve editor with default settings.
/// </summary>
/// <seealso cref="Settings.defaultSettings"/>
public InspectorCurveEditor()
: this(Settings.defaultSettings) {}
/// <summary>
/// Creates a curve editor with the given settings.
/// </summary>
/// <param name="settings">The settings to use to create the curve editor.</param>
public InspectorCurveEditor(Settings settings)
{
this.settings = settings;
m_Curves = new Dictionary<SerializedProperty, CurveState>();
}
/// <summary>
/// Adds an arbitrary number of serialized curves to the editor.
/// </summary>
/// <param name="curves">The curves to add.</param>
public void Add(params SerializedProperty[] curves)
{
foreach (var curve in curves)
Add(curve, CurveState.defaultState);
}
/// <summary>
/// Adds a serialized curve to the editor.
/// </summary>
/// <param name="curve">The curve to add.</param>
public void Add(SerializedProperty curve)
{
Add(curve, CurveState.defaultState);
}
/// <summary>
/// Adds a serialized curve to the editor with a given state.
/// </summary>
/// <param name="curve">The curve to add.</param>
/// <param name="state">The state to use for the curve.</param>
public void Add(SerializedProperty curve, CurveState state)
{
// Make sure the property is in fact an AnimationCurve
var animCurve = curve.animationCurveValue;
if (animCurve == null)
throw new ArgumentException("curve");
if (m_Curves.ContainsKey(curve))
Debug.LogWarning("Curve has already been added to the editor");
m_Curves.Add(curve, state);
}
/// <summary>
/// Removes a single curve from the editor.
/// </summary>
/// <param name="curve">The curve to remove.</param>
public void Remove(SerializedProperty curve)
{
m_Curves.Remove(curve);
}
/// <summary>
/// Removes all the curve from the editor.
/// </summary>
public void RemoveAll()
{
m_Curves.Clear();
}
/// <summary>
/// Grabs the state for a given curve.
/// </summary>
/// <param name="curve">The curve to grab the state from.</param>
/// <returns>The state of the curve.</returns>
public CurveState GetCurveState(SerializedProperty curve)
{
if (!m_Curves.TryGetValue(curve, out var state))
throw new KeyNotFoundException("curve");
return state;
}
/// <summary>
/// Sets the state for a given curve.
/// </summary>
/// <param name="curve">The curve to set the state of.</param>
/// <param name="state">The state to set for the curve.</param>
public void SetCurveState(SerializedProperty curve, CurveState state)
{
if (!m_Curves.ContainsKey(curve))
throw new KeyNotFoundException("curve");
m_Curves[curve] = state;
}
/// <summary>
/// Gets the current selection.
/// </summary>
/// <returns>The current selection.</returns>
public Selection GetSelection()
{
Keyframe? key = null;
if (m_SelectedKeyframeIndex > -1)
{
var curve = m_SelectedCurve.animationCurveValue;
if (m_SelectedKeyframeIndex >= curve.length)
m_SelectedKeyframeIndex = -1;
else
key = curve[m_SelectedKeyframeIndex];
}
return new Selection(m_SelectedCurve, m_SelectedKeyframeIndex, key);
}
/// <summary>
/// Sets a key for a given curve.
/// </summary>
/// <param name="curve">The curve to modify.</param>
/// <param name="keyframeIndex">The index of the key to set.</param>
/// <param name="keyframe">The new keyframe to put at the index.</param>
public void SetKeyframe(SerializedProperty curve, int keyframeIndex, Keyframe keyframe)
{
var animCurve = curve.animationCurveValue;
SetKeyframe(animCurve, keyframeIndex, keyframe);
SaveCurve(curve, animCurve);
}
/// <summary>
/// Draws the curve editor. This is meant to be called in your custom editors.
/// </summary>
/// <param name="rect">The rectangle to draw into.</param>
/// <returns><c>true</c> if the user modified the curve, <c>false</c> otherwise.</returns>
public bool OnGUI(Rect rect)
{
if (Event.current.type == EventType.Repaint)
m_Dirty = false;
GUI.BeginClip(rect);
{
var area = new Rect(Vector2.zero, rect.size);
m_CurveArea = settings.padding.Remove(area);
foreach (var curve in m_Curves)
OnCurveGUI(area, curve.Key, curve.Value);
OnGeneralUI();
}
GUI.EndClip();
return m_Dirty;
}
void OnCurveGUI(Rect rect, SerializedProperty curve, CurveState state)
{
// Discard invisible curves
if (!state.visible)
return;
var animCurve = curve.animationCurveValue;
var keys = animCurve.keys;
var length = keys.Length;
// Curve drawing
// Slightly dim non-editable curves
var color = state.color;
if (!state.editable || !GUI.enabled)
color.a *= 0.5f;
Handles.color = color;
var bounds = settings.bounds;
if (length == 0)
{
var p1 = CurveToCanvas(new Vector3(bounds.xMin, state.zeroKeyConstantValue));
var p2 = CurveToCanvas(new Vector3(bounds.xMax, state.zeroKeyConstantValue));
Handles.DrawAAPolyLine(state.width, p1, p2);
}
else if (length == 1)
{
var p1 = CurveToCanvas(new Vector3(bounds.xMin, keys[0].value));
var p2 = CurveToCanvas(new Vector3(bounds.xMax, keys[0].value));
Handles.DrawAAPolyLine(state.width, p1, p2);
}
else
{
var prevKey = keys[0];
for (int k = 1; k < length; k++)
{
var key = keys[k];
var pts = BezierSegment(prevKey, key);
if (float.IsInfinity(prevKey.outTangent) || float.IsInfinity(key.inTangent))
{
var s = HardSegment(prevKey, key);
Handles.DrawAAPolyLine(state.width, s[0], s[1], s[2]);
}
else Handles.DrawBezier(pts[0], pts[3], pts[1], pts[2], color, null, state.width);
prevKey = key;
}
// Curve extents & loops
if (keys[0].time > bounds.xMin)
{
if (state.loopInBounds)
{
var p1 = keys[length - 1];
p1.time -= settings.bounds.width;
var p2 = keys[0];
var pts = BezierSegment(p1, p2);
if (float.IsInfinity(p1.outTangent) || float.IsInfinity(p2.inTangent))
{
var s = HardSegment(p1, p2);
Handles.DrawAAPolyLine(state.width, s[0], s[1], s[2]);
}
else Handles.DrawBezier(pts[0], pts[3], pts[1], pts[2], color, null, state.width);
}
else
{
var p1 = CurveToCanvas(new Vector3(bounds.xMin, keys[0].value));
var p2 = CurveToCanvas(keys[0]);
Handles.DrawAAPolyLine(state.width, p1, p2);
}
}
if (keys[length - 1].time < bounds.xMax)
{
if (state.loopInBounds)
{
var p1 = keys[length - 1];
var p2 = keys[0];
p2.time += settings.bounds.width;
var pts = BezierSegment(p1, p2);
if (float.IsInfinity(p1.outTangent) || float.IsInfinity(p2.inTangent))
{
var s = HardSegment(p1, p2);
Handles.DrawAAPolyLine(state.width, s[0], s[1], s[2]);
}
else Handles.DrawBezier(pts[0], pts[3], pts[1], pts[2], color, null, state.width);
}
else
{
var p1 = CurveToCanvas(keys[length - 1]);
var p2 = CurveToCanvas(new Vector3(bounds.xMax, keys[length - 1].value));
Handles.DrawAAPolyLine(state.width, p1, p2);
}
}
}
// Make sure selection is correct (undo can break it)
bool isCurrentlySelectedCurve = curve == m_SelectedCurve;
if (isCurrentlySelectedCurve && m_SelectedKeyframeIndex >= length)
m_SelectedKeyframeIndex = -1;
if (!state.editable)
m_SelectedKeyframeIndex = -1;
float enabledFactor = GUI.enabled ? 1f : 0.8f;
// Handles & keys
for (int k = 0; k < length; k++)
{
bool isCurrentlySelectedKeyframe = k == m_SelectedKeyframeIndex;
var e = Event.current;
var pos = CurveToCanvas(keys[k]);
var hitRect = new Rect(pos.x - 8f, pos.y - 8f, 16f, 16f);
var offset = isCurrentlySelectedCurve
? new RectOffset(5, 5, 5, 5)
: new RectOffset(6, 6, 6, 6);
var outTangent = pos + CurveTangentToCanvas(keys[k].outTangent).normalized * 40f;
var inTangent = pos - CurveTangentToCanvas(keys[k].inTangent).normalized * 40f;
var inTangentHitRect = new Rect(inTangent.x - 7f, inTangent.y - 7f, 14f, 14f);
var outTangentHitrect = new Rect(outTangent.x - 7f, outTangent.y - 7f, 14f, 14f);
// Draw
if (state.editable || state.showNonEditableHandles)
{
if (e.type == EventType.Repaint)
{
var selectedColor = (isCurrentlySelectedCurve && isCurrentlySelectedKeyframe)
? settings.selectionColor
: state.color;
// Keyframe
EditorGUI.DrawRect(offset.Remove(hitRect), selectedColor * enabledFactor);
// Tangents
if (length > 1 && isCurrentlySelectedCurve && (!state.onlyShowHandlesOnSelection || (state.onlyShowHandlesOnSelection && isCurrentlySelectedKeyframe)))
{
Handles.color = selectedColor * enabledFactor;
if (k > 0 || state.loopInBounds)
{
Handles.DrawAAPolyLine(state.handleWidth, pos, inTangent);
EditorGUI.DrawRect(offset.Remove(inTangentHitRect), selectedColor);
}
if (k < length - 1 || state.loopInBounds)
{
Handles.DrawAAPolyLine(state.handleWidth, pos, outTangent);
EditorGUI.DrawRect(offset.Remove(outTangentHitrect), selectedColor);
}
}
}
}
// Events
if (state.editable)
{
// Keyframe move
if (m_EditMode == EditMode.Moving && e.type == EventType.MouseDrag && isCurrentlySelectedCurve && isCurrentlySelectedKeyframe)
{
EditMoveKeyframe(animCurve, keys, k);
}
// Tangent editing
if (length > 1 && m_EditMode == EditMode.TangentEdit && e.type == EventType.MouseDrag && isCurrentlySelectedCurve && isCurrentlySelectedKeyframe)
{
bool alreadyBroken = !(Mathf.Approximately(keys[k].inTangent, keys[k].outTangent) || (float.IsInfinity(keys[k].inTangent) && float.IsInfinity(keys[k].outTangent)));
EditMoveTangent(animCurve, keys, k, m_TangentEditMode, e.shift || !(alreadyBroken || e.control));
}
// Keyframe selection & context menu
if (e.type == EventType.MouseDown && rect.Contains(e.mousePosition))
{
if (hitRect.Contains(e.mousePosition))
{
if (e.button == 0)
{
SelectKeyframe(curve, k);
m_EditMode = EditMode.Moving;
e.Use();
}
else if (e.button == 1)
{
// Keyframe context menu
var menu = new GenericMenu();
menu.AddItem(new GUIContent("Delete Key"), false, (x) =>
{
var action = (MenuAction)x;
var curveValue = action.curve.animationCurveValue;
action.curve.serializedObject.Update();
RemoveKeyframe(curveValue, action.index);
m_SelectedKeyframeIndex = -1;
SaveCurve(action.curve, curveValue);
action.curve.serializedObject.ApplyModifiedProperties();
}, new MenuAction(curve, k));
menu.ShowAsContext();
e.Use();
}
}
}
// Tangent selection & edit mode
if (e.type == EventType.MouseDown && length > 1 && rect.Contains(e.mousePosition))
{
if (inTangentHitRect.Contains(e.mousePosition) && (k > 0 || state.loopInBounds))
{
SelectKeyframe(curve, k);
m_EditMode = EditMode.TangentEdit;
m_TangentEditMode = Tangent.In;
e.Use();
}
else if (outTangentHitrect.Contains(e.mousePosition) && (k < length - 1 || state.loopInBounds))
{
SelectKeyframe(curve, k);
m_EditMode = EditMode.TangentEdit;
m_TangentEditMode = Tangent.Out;
e.Use();
}
}
// Mouse up - clean up states
if (e.rawType == EventType.MouseUp && m_EditMode != EditMode.None)
{
m_EditMode = EditMode.None;
}
// Set cursors
{
EditorGUIUtility.AddCursorRect(hitRect, MouseCursor.MoveArrow);
if (k > 0 || state.loopInBounds)
EditorGUIUtility.AddCursorRect(inTangentHitRect, MouseCursor.RotateArrow);
if (k < length - 1 || state.loopInBounds)
EditorGUIUtility.AddCursorRect(outTangentHitrect, MouseCursor.RotateArrow);
}
}
}
Handles.color = Color.white;
SaveCurve(curve, animCurve);
}
void OnGeneralUI()
{
var e = Event.current;
// Selection
if (e.type == EventType.MouseDown)
{
GUI.FocusControl(null);
m_SelectedCurve = null;
m_SelectedKeyframeIndex = -1;
bool used = false;
var hit = CanvasToCurve(e.mousePosition);
float curvePickValue = CurveToCanvas(hit).y;
// Try and select a curve
foreach (var curve in m_Curves)
{
if (!curve.Value.editable || !curve.Value.visible)
continue;
var prop = curve.Key;
var state = curve.Value;
var animCurve = prop.animationCurveValue;
float hitY = animCurve.length == 0
? state.zeroKeyConstantValue
: animCurve.Evaluate(hit.x);
var curvePos = CurveToCanvas(new Vector3(hit.x, hitY));
if (Mathf.Abs(curvePos.y - curvePickValue) < settings.curvePickingDistance)
{
m_SelectedCurve = prop;
if (e.clickCount == 2 && e.button == 0)
{
// Create a keyframe on double-click on this curve
EditCreateKeyframe(animCurve, hit, true, state.zeroKeyConstantValue);
SaveCurve(prop, animCurve);
}
else if (e.button == 1)
{
// Curve context menu
var menu = new GenericMenu();
menu.AddItem(new GUIContent("Add Key"), false, (x) =>
{
var action = (MenuAction)x;
var curveValue = action.curve.animationCurveValue;
action.curve.serializedObject.Update();
EditCreateKeyframe(curveValue, hit, true, 0f);
SaveCurve(action.curve, curveValue);
action.curve.serializedObject.ApplyModifiedProperties();
}, new MenuAction(prop, hit));
menu.ShowAsContext();
e.Use();
used = true;
}
}
}
if (e.clickCount == 2 && e.button == 0 && m_SelectedCurve == null)
{
// Create a keyframe on every curve on double-click
foreach (var curve in m_Curves)
{
if (!curve.Value.editable || !curve.Value.visible)
continue;
var prop = curve.Key;
var state = curve.Value;
var animCurve = prop.animationCurveValue;
EditCreateKeyframe(animCurve, hit, e.alt, state.zeroKeyConstantValue);
SaveCurve(prop, animCurve);
}
}
else if (!used && e.button == 1)
{
// Global context menu
var menu = new GenericMenu();
menu.AddItem(new GUIContent("Add Key At Position"), false, () => ContextMenuAddKey(hit, false));
menu.AddItem(new GUIContent("Add Key On Curves"), false, () => ContextMenuAddKey(hit, true));
menu.ShowAsContext();
}
e.Use();
}
// Delete selected key(s)
if (e.type == EventType.KeyDown && (e.keyCode == KeyCode.Delete || e.keyCode == KeyCode.Backspace))
{
if (m_SelectedKeyframeIndex != -1 && m_SelectedCurve != null)
{
var animCurve = m_SelectedCurve.animationCurveValue;
var length = animCurve.length;
if (m_Curves[m_SelectedCurve].minPointCount < length && length >= 0)
{
EditDeleteKeyframe(animCurve, m_SelectedKeyframeIndex);
m_SelectedKeyframeIndex = -1;
SaveCurve(m_SelectedCurve, animCurve);
}
e.Use();
}
}
}
void SaveCurve(SerializedProperty prop, AnimationCurve curve)
{
prop.animationCurveValue = curve;
}
void Invalidate()
{
m_Dirty = true;
}
void SelectKeyframe(SerializedProperty curve, int keyframeIndex)
{
m_SelectedKeyframeIndex = keyframeIndex;
m_SelectedCurve = curve;
Invalidate();
}
void ContextMenuAddKey(Vector3 hit, bool createOnCurve)
{
SerializedObject serializedObject = null;
foreach (var curve in m_Curves)
{
if (!curve.Value.editable || !curve.Value.visible)
continue;
var prop = curve.Key;
var state = curve.Value;
if (serializedObject == null)
{
serializedObject = prop.serializedObject;
serializedObject.Update();
}
var animCurve = prop.animationCurveValue;
EditCreateKeyframe(animCurve, hit, createOnCurve, state.zeroKeyConstantValue);
SaveCurve(prop, animCurve);
}
if (serializedObject != null)
serializedObject.ApplyModifiedProperties();
Invalidate();
}
void EditCreateKeyframe(AnimationCurve curve, Vector3 position, bool createOnCurve, float zeroKeyConstantValue)
{
float tangent = EvaluateTangent(curve, position.x);
if (createOnCurve)
{
position.y = curve.length == 0
? zeroKeyConstantValue
: curve.Evaluate(position.x);
}
AddKeyframe(curve, new Keyframe(position.x, position.y, tangent, tangent));
}
void EditDeleteKeyframe(AnimationCurve curve, int keyframeIndex)
{
RemoveKeyframe(curve, keyframeIndex);
}
void AddKeyframe(AnimationCurve curve, Keyframe newValue)
{
curve.AddKey(newValue);
Invalidate();
}
void RemoveKeyframe(AnimationCurve curve, int keyframeIndex)
{
curve.RemoveKey(keyframeIndex);
Invalidate();
}
void SetKeyframe(AnimationCurve curve, int keyframeIndex, Keyframe newValue)
{
var keys = curve.keys;
if (keyframeIndex > 0)
newValue.time = Mathf.Max(keys[keyframeIndex - 1].time + settings.keyTimeClampingDistance, newValue.time);
if (keyframeIndex < keys.Length - 1)
newValue.time = Mathf.Min(keys[keyframeIndex + 1].time - settings.keyTimeClampingDistance, newValue.time);
curve.MoveKey(keyframeIndex, newValue);
Invalidate();
}
void EditMoveKeyframe(AnimationCurve curve, Keyframe[] keys, int keyframeIndex)
{
var key = CanvasToCurve(Event.current.mousePosition);
float inTgt = keys[keyframeIndex].inTangent;
float outTgt = keys[keyframeIndex].outTangent;
SetKeyframe(curve, keyframeIndex, new Keyframe(key.x, key.y, inTgt, outTgt));
}
void EditMoveTangent(AnimationCurve curve, Keyframe[] keys, int keyframeIndex, Tangent targetTangent, bool linkTangents)
{
var pos = CanvasToCurve(Event.current.mousePosition);
float time = keys[keyframeIndex].time;
float value = keys[keyframeIndex].value;
pos -= new Vector3(time, value);
if (targetTangent == Tangent.In && pos.x > 0f)
pos.x = 0f;
if (targetTangent == Tangent.Out && pos.x < 0f)
pos.x = 0f;
float tangent;
if (Mathf.Approximately(pos.x, 0f))
tangent = float.PositiveInfinity;
else
tangent = pos.y / pos.x;
float inTangent = keys[keyframeIndex].inTangent;
float outTangent = keys[keyframeIndex].outTangent;
if (targetTangent == Tangent.In || linkTangents)
inTangent = tangent;
if (targetTangent == Tangent.Out || linkTangents)
outTangent = tangent;
SetKeyframe(curve, keyframeIndex, new Keyframe(time, value, inTangent, outTangent));
}
Vector3 CurveToCanvas(Keyframe keyframe)
{
return CurveToCanvas(new Vector3(keyframe.time, keyframe.value));
}
Vector3 CurveToCanvas(Vector3 position)
{
var bounds = settings.bounds;
var output = new Vector3((position.x - bounds.x) / (bounds.xMax - bounds.x), (position.y - bounds.y) / (bounds.yMax - bounds.y));
output.x = output.x * (m_CurveArea.xMax - m_CurveArea.xMin) + m_CurveArea.xMin;
output.y = (1f - output.y) * (m_CurveArea.yMax - m_CurveArea.yMin) + m_CurveArea.yMin;
return output;
}
Vector3 CanvasToCurve(Vector3 position)
{
var bounds = settings.bounds;
var output = position;
output.x = (output.x - m_CurveArea.xMin) / (m_CurveArea.xMax - m_CurveArea.xMin);
output.y = (output.y - m_CurveArea.yMin) / (m_CurveArea.yMax - m_CurveArea.yMin);
output.x = Mathf.Lerp(bounds.x, bounds.xMax, output.x);
output.y = Mathf.Lerp(bounds.yMax, bounds.y, output.y);
return output;
}
Vector3 CurveTangentToCanvas(float tangent)
{
if (!float.IsInfinity(tangent))
{
var bounds = settings.bounds;
float ratio = (m_CurveArea.width / m_CurveArea.height) / ((bounds.xMax - bounds.x) / (bounds.yMax - bounds.y));
return new Vector3(1f, -tangent / ratio).normalized;
}
return Vector3.up; // Positive infinity
}
Vector3[] BezierSegment(Keyframe start, Keyframe end)
{
var segment = new Vector3[4];
segment[0] = CurveToCanvas(new Vector3(start.time, start.value));
segment[3] = CurveToCanvas(new Vector3(end.time, end.value));
float middle = start.time + ((end.time - start.time) * 0.333333f);
float middle2 = start.time + ((end.time - start.time) * 0.666666f);
segment[1] = CurveToCanvas(new Vector3(middle, ProjectTangent(start.time, start.value, start.outTangent, middle)));
segment[2] = CurveToCanvas(new Vector3(middle2, ProjectTangent(end.time, end.value, end.inTangent, middle2)));
return segment;
}
Vector3[] HardSegment(Keyframe start, Keyframe end)
{
var segment = new Vector3[3];
segment[0] = CurveToCanvas(start);
segment[1] = CurveToCanvas(new Vector3(end.time, start.value));
segment[2] = CurveToCanvas(end);
return segment;
}
float ProjectTangent(float inPosition, float inValue, float inTangent, float projPosition)
{
return inValue + ((projPosition - inPosition) * inTangent);
}
float EvaluateTangent(AnimationCurve curve, float time)
{
int prev = -1, next = 0;
for (int i = 0; i < curve.keys.Length; i++)
{
if (time > curve.keys[i].time)
{
prev = i;
next = i + 1;
}
else break;
}
if (next == 0)
return 0f;
if (prev == curve.keys.Length - 1)
return 0f;
const float kD = 1e-3f;
float tp = Mathf.Max(time - kD, curve.keys[prev].time);
float tn = Mathf.Min(time + kD, curve.keys[next].time);
float vp = curve.Evaluate(tp);
float vn = curve.Evaluate(tn);
if (Mathf.Approximately(tn, tp))
return float.PositiveInfinity;
return (vn - vp) / (tn - tp);
}
}
}

View File

@@ -0,0 +1,679 @@
using System;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
using UnityEngine.Rendering;
namespace UnityEditor.Rendering
{
/// <summary>Utility class for drawing light Editor gizmos</summary>
public static class CoreLightEditorUtilities
{
[Flags]
enum HandleDirections
{
Left = 1 << 0,
Up = 1 << 1,
Right = 1 << 2,
Down = 1 << 3,
All = Left | Up | Right | Down
}
static readonly Vector3[] directionalLightHandlesRayPositions =
{
new Vector3(1, 0, 0),
new Vector3(-1, 0, 0),
new Vector3(0, 1, 0),
new Vector3(0, -1, 0),
new Vector3(1, 1, 0).normalized,
new Vector3(1, -1, 0).normalized,
new Vector3(-1, 1, 0).normalized,
new Vector3(-1, -1, 0).normalized
};
/// <summary>
/// Draw a gizmo for a directional light.
/// </summary>
/// <param name="light">The light that is used for this gizmo.</param>
public static void DrawDirectionalLightGizmo(Light light)
{
// Sets the color of the Gizmo.
Color outerColor = GetLightAboveObjectWireframeColor(light.color);
Vector3 lightPos = light.transform.position;
float lightSize;
using (new Handles.DrawingScope(Matrix4x4.identity)) //be sure no matrix affect the size computation
{
lightSize = HandleUtility.GetHandleSize(lightPos);
}
float radius = lightSize * 0.2f;
using (new Handles.DrawingScope(outerColor))
{
Handles.DrawWireDisc(Vector3.zero, Vector3.forward, radius);
foreach (Vector3 normalizedPos in directionalLightHandlesRayPositions)
{
Vector3 pos = normalizedPos * radius;
Handles.DrawLine(pos, pos + new Vector3(0, 0, lightSize));
}
}
}
/// <summary>
/// Draw a gizmo for a point light.
/// </summary>
/// <param name="light">The light that is used for this gizmo.</param>
public static void DrawPointLightGizmo(Light light)
{
// Sets the color of the Gizmo.
Color outerColor = GetLightAboveObjectWireframeColor(light.color);
// Drawing the point light
DrawPointLight(light, outerColor);
// Draw the handles and labels
DrawPointHandlesAndLabels(light);
}
static void DrawPointLight(Light light, Color outerColor)
{
float range = light.range;
using (new Handles.DrawingScope(outerColor))
{
EditorGUI.BeginChangeCheck();
//range = Handles.RadiusHandle(Quaternion.identity, light.transform.position, range, false, true);
range = DoPointHandles(range);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(light, "Adjust Point Light");
light.range = range;
}
}
}
static void DrawPointHandlesAndLabels(Light light)
{
// Getting the first control on point handle
var firstControl = GUIUtility.GetControlID(s_PointLightHandle.GetHashCode(), FocusType.Passive) - 6; // BoxBoundsHandle allocates 6 control IDs
if (Event.current.type != EventType.Repaint)
return;
// var firstControl = GUIUtility.GetControlID(k_RadiusHandleHash, FocusType.Passive) - 6;
// if (Event.current.type != EventType.Repaint)
// return;
// Adding label /////////////////////////////////////
Vector3 labelPosition = Vector3.zero;
if (GUIUtility.hotControl != 0)
{
switch (GUIUtility.hotControl - firstControl)
{
case 0:
labelPosition = Vector3.right * light.range;
break;
case 1:
labelPosition = Vector3.left * light.range;
break;
case 2:
labelPosition = Vector3.up * light.range;
break;
case 3:
labelPosition = Vector3.down * light.range;
break;
case 4:
labelPosition = Vector3.forward * light.range;
break;
case 5:
labelPosition = Vector3.back * light.range;
break;
default:
return;
}
string labelText = FormattableString.Invariant($"Range: {light.range:0.00}");
DrawHandleLabel(labelPosition, labelText);
}
}
/// <summary>
/// Draw a gizmo for an area/rectangle light.
/// </summary>
/// <param name="light">The light that is used for this gizmo.</param>
public static void DrawRectangleLightGizmo(Light light)
{
// Color to use for gizmo drawing
Color outerColor = GetLightAboveObjectWireframeColor(light.color);
// Drawing the gizmo
DrawRectangleLight(light, outerColor);
// Draw the handles and labels
DrawRectangleHandlesAndLabels(light);
}
static void DrawRectangleLight(Light light, Color outerColor)
{
Vector2 size = light.areaSize;
var range = light.range;
var innerColor = GetLightBehindObjectWireframeColor(light.color);
DrawZTestedLine(range, outerColor, innerColor);
using (new Handles.DrawingScope(outerColor))
{
EditorGUI.BeginChangeCheck();
size = DoRectHandles(size);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(light, "Adjust Area Rectangle Light");
light.areaSize = size;
}
}
}
static void DrawRectangleHandlesAndLabels(Light light)
{
// Getting the first control on radius handle
var firstControl = GUIUtility.GetControlID(s_AreaLightHandle.GetHashCode(), FocusType.Passive) - 6; // BoxBoundsHandle allocates 6 control IDs
if (Event.current.type != EventType.Repaint)
return;
// Adding label /////////////////////////////////////
Vector3 labelPosition = Vector3.zero;
if (GUIUtility.hotControl != 0)
{
switch (GUIUtility.hotControl - firstControl)
{
case 0: // PositiveX
labelPosition = Vector3.right * (light.areaSize.x / 2);
break;
case 1: // NegativeX
labelPosition = Vector3.left * (light.areaSize.x / 2);
break;
case 2: // PositiveY
labelPosition = Vector3.up * (light.areaSize.y / 2);
break;
case 3: // NegativeY
labelPosition = Vector3.down * (light.areaSize.y / 2);
break;
default:
return;
}
string labelText = FormattableString.Invariant($"w:{light.areaSize.x:0.00} x h:{light.areaSize.y:0.00}");
DrawHandleLabel(labelPosition, labelText);
}
}
/// <summary>
/// Draw a gizmo for a disc light.
/// </summary>
/// <param name="light">The light that is used for this gizmo.</param>
public static void DrawDiscLightGizmo(Light light)
{
// Color to use for gizmo drawing.
Color outerColor = GetLightAboveObjectWireframeColor(light.color);
// Drawing before objects
DrawDiscLight(light, outerColor);
// Draw handles
DrawDiscHandlesAndLabels(light);
}
static void DrawDiscLight(Light light, Color outerColor)
{
float radius = light.areaSize.x;
var range = light.range;
var innerColor = GetLightBehindObjectWireframeColor(light.color);
DrawZTestedLine(range, outerColor, innerColor);
using (new Handles.DrawingScope(outerColor))
{
EditorGUI.BeginChangeCheck();
radius = DoDiscHandles(radius);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(light, "Adjust Area Disc Light");
light.areaSize = new Vector2(radius, light.areaSize.y);
}
}
}
static void DrawDiscHandlesAndLabels(Light light)
{
// Getting the first control on radius handle
var firstControl = GUIUtility.GetControlID(s_DiscLightHandle.GetHashCode(), FocusType.Passive) - 6; // BoxBoundsHandle allocates 6 control IDs
if (Event.current.type != EventType.Repaint)
return;
Vector3 labelPosition = Vector3.zero;
if (GUIUtility.hotControl != 0)
{
switch (GUIUtility.hotControl - firstControl)
{
case 0: // PositiveX
labelPosition = Vector3.right * light.areaSize.x;
break;
case 1: // NegativeX
labelPosition = Vector3.left * light.areaSize.x;
break;
case 2: // PositiveY
labelPosition = Vector3.up * light.areaSize.x;
break;
case 3: // NegativeY
labelPosition = Vector3.down * light.areaSize.x;
break;
default:
return;
}
string labelText = FormattableString.Invariant($"Radius: {light.areaSize.x:0.00}");
DrawHandleLabel(labelPosition, labelText);
}
}
static void DrawWithZTest(PrimitiveBoundsHandle primitiveHandle, float alpha = 0.2f)
{
primitiveHandle.center = Vector3.zero;
primitiveHandle.handleColor = Color.clear;
primitiveHandle.wireframeColor = Color.white;
Handles.zTest = CompareFunction.LessEqual;
primitiveHandle.DrawHandle();
primitiveHandle.wireframeColor = new Color(1f, 1f, 1f, alpha);
Handles.zTest = CompareFunction.Greater;
primitiveHandle.DrawHandle();
primitiveHandle.handleColor = Color.white;
primitiveHandle.wireframeColor = Color.clear;
Handles.zTest = CompareFunction.Always;
primitiveHandle.DrawHandle();
}
static void DrawZTestedLine(float range, Color outerColor, Color innerColor)
{
using (new Handles.DrawingScope(outerColor))
{
Handles.zTest = CompareFunction.LessEqual;
Handles.DrawLine(Vector3.zero, Vector3.forward * range);
}
using (new Handles.DrawingScope(innerColor))
{
Handles.zTest = CompareFunction.Greater;
Handles.DrawLine(Vector3.zero, Vector3.forward * range);
}
Handles.zTest = CompareFunction.Always;
}
static void DrawHandleLabel(Vector3 handlePosition, string labelText, float offsetFromHandle = 0.3f)
{
Vector3 labelPosition = Vector3.zero;
var style = new GUIStyle {normal = {background = Texture2D.whiteTexture}};
GUI.color = new Color(0.82f, 0.82f, 0.82f, 1);
labelPosition = handlePosition + Handles.inverseMatrix.MultiplyVector(Vector3.up) * HandleUtility.GetHandleSize(handlePosition) * offsetFromHandle;
Handles.Label(labelPosition, labelText, style);
}
static readonly BoxBoundsHandle s_AreaLightHandle =
new BoxBoundsHandle { axes = PrimitiveBoundsHandle.Axes.X | PrimitiveBoundsHandle.Axes.Y };
static Vector2 DoRectHandles(Vector2 size)
{
s_AreaLightHandle.center = Vector3.zero;
s_AreaLightHandle.size = size;
DrawWithZTest(s_AreaLightHandle);
return s_AreaLightHandle.size;
}
static readonly SphereBoundsHandle s_DiscLightHandle =
new SphereBoundsHandle { axes = PrimitiveBoundsHandle.Axes.X | PrimitiveBoundsHandle.Axes.Y };
static float DoDiscHandles(float radius)
{
s_DiscLightHandle.center = Vector3.zero;
s_DiscLightHandle.radius = radius;
DrawWithZTest(s_DiscLightHandle);
return s_DiscLightHandle.radius;
}
static readonly SphereBoundsHandle s_PointLightHandle =
new SphereBoundsHandle { axes = PrimitiveBoundsHandle.Axes.All };
static float DoPointHandles(float range)
{
s_PointLightHandle.radius = range;
DrawWithZTest(s_PointLightHandle);
return s_PointLightHandle.radius;
}
static bool drawInnerConeAngle = true;
/// <summary>
/// Draw a gizmo for a spot light.
/// </summary>
/// <param name="light">The light that is used for this gizmo.</param>
public static void DrawSpotLightGizmo(Light light)
{
// Saving the default colors
var defColor = Handles.color;
var defZTest = Handles.zTest;
// Default Color for outer cone will be Yellow if nothing has been provided.
Color outerColor = GetLightAboveObjectWireframeColor(light.color);
// The default z-test outer color will be 20% opacity of the outer color
Color outerColorZTest = GetLightBehindObjectWireframeColor(outerColor);
// Default Color for inner cone will be Yellow-ish if nothing has been provided.
Color innerColor = GetLightInnerConeColor(light.color);
// The default z-test outer color will be 20% opacity of the inner color
Color innerColorZTest = GetLightBehindObjectWireframeColor(innerColor);
// Drawing before objects
Handles.zTest = CompareFunction.LessEqual;
DrawSpotlightWireframe(light, outerColor, innerColor);
// Drawing behind objects
Handles.zTest = CompareFunction.Greater;
DrawSpotlightWireframe(light, outerColorZTest, innerColorZTest);
// Resets the compare function to always
Handles.zTest = CompareFunction.Always;
// Draw handles
if (!Event.current.alt)
{
DrawHandlesAndLabels(light, outerColor);
}
// Resets the handle colors
Handles.color = defColor;
Handles.zTest = defZTest;
}
static void DrawHandlesAndLabels(Light light, Color color)
{
// Zero position vector3
Vector3 zeroPos = Vector3.zero;
// Variable for which direction to draw the handles
HandleDirections DrawHandleDirections;
// Draw the handles ///////////////////////////////
Handles.color = color;
// Draw Center Handle
float range = light.range;
var id = GUIUtility.GetControlID(FocusType.Passive);
EditorGUI.BeginChangeCheck();
range = SliderLineHandle(id, Vector3.zero, Vector3.forward, range, "Range: ");
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObjects(new[] { light }, "Undo range change.");
}
// Draw outer handles
DrawHandleDirections = HandleDirections.Down | HandleDirections.Up;
const string outerLabel = "Outer Angle: ";
EditorGUI.BeginChangeCheck();
float outerAngle = DrawConeHandles(zeroPos, light.spotAngle, range, DrawHandleDirections, outerLabel);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObjects(new[] { light }, "Undo outer angle change.");
}
// Draw inner handles
float innerAngle = 0;
const string innerLabel = "Inner Angle: ";
if (light.innerSpotAngle > 0f && drawInnerConeAngle)
{
DrawHandleDirections = HandleDirections.Left | HandleDirections.Right;
EditorGUI.BeginChangeCheck();
innerAngle = DrawConeHandles(zeroPos, light.innerSpotAngle, range, DrawHandleDirections, innerLabel);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObjects(new[] { light }, "Undo inner angle change.");
}
}
// Draw Near Plane Handle
float nearPlaneRange = light.shadowNearPlane;
if (light.shadows != LightShadows.None && light.lightmapBakeType != LightmapBakeType.Baked)
{
EditorGUI.BeginChangeCheck();
nearPlaneRange = SliderLineHandle(GUIUtility.GetControlID(FocusType.Passive), Vector3.zero, Vector3.forward, nearPlaneRange, "Near Plane: ");
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObjects(new[] { light }, "Undo shadow near plane change.");
nearPlaneRange = Mathf.Clamp(nearPlaneRange, 0.1f, light.range);
}
}
// If changes has been made we update the corresponding property
if (GUI.changed)
{
light.spotAngle = outerAngle;
light.innerSpotAngle = innerAngle;
light.range = Math.Max(range, 0.01f);
light.shadowNearPlane = Mathf.Clamp(nearPlaneRange, 0.1f, light.range);
}
}
static Color GetLightInnerConeColor(Color wireframeColor)
{
Color color = wireframeColor;
color.a = 0.4f;
return RemapLightColor(CoreUtils.ConvertLinearToActiveColorSpace(color.linear));
}
static Color GetLightAboveObjectWireframeColor(Color wireframeColor)
{
Color color = wireframeColor;
color.a = 1f;
return RemapLightColor(CoreUtils.ConvertLinearToActiveColorSpace(color.linear));
}
static Color GetLightBehindObjectWireframeColor(Color wireframeColor)
{
Color color = wireframeColor;
color.a = 0.2f;
return RemapLightColor(CoreUtils.ConvertLinearToActiveColorSpace(color.linear));
}
static Color RemapLightColor(Color src)
{
Color color = src;
float max = Mathf.Max(Mathf.Max(color.r, color.g), color.b);
if (max > 0f)
{
float mult = 1f / max;
color.r *= mult;
color.g *= mult;
color.b *= mult;
}
else
{
color = Color.white;
}
return color;
}
static void DrawSpotlightWireframe(Light spotlight, Color outerColor, Color innerColor)
{
// Variable for which direction to draw the handles
HandleDirections DrawHandleDirections;
float outerAngle = spotlight.spotAngle;
float innerAngle = spotlight.innerSpotAngle;
float range = spotlight.range;
var outerDiscRadius = range * Mathf.Sin(outerAngle * Mathf.Deg2Rad * 0.5f);
var outerDiscDistance = Mathf.Cos(Mathf.Deg2Rad * outerAngle * 0.5f) * range;
var vectorLineUp = Vector3.Normalize(Vector3.forward * outerDiscDistance + Vector3.up * outerDiscRadius);
var vectorLineLeft = Vector3.Normalize(Vector3.forward * outerDiscDistance + Vector3.left * outerDiscRadius);
// Need to check if we need to draw inner angle
// Need to disable this for now until we get all the inner angle baking working.
if (innerAngle > 0f && drawInnerConeAngle)
{
DrawHandleDirections = HandleDirections.Up | HandleDirections.Down;
var innerDiscRadius = range * Mathf.Sin(innerAngle * Mathf.Deg2Rad * 0.5f);
var innerDiscDistance = Mathf.Cos(Mathf.Deg2Rad * innerAngle * 0.5f) * range;
// Drawing the inner Cone and also z-testing it to draw another color if behind
Handles.color = innerColor;
DrawConeWireframe(innerDiscRadius, innerDiscDistance, DrawHandleDirections);
}
// Draw range line
Handles.color = innerColor;
var rangeCenter = Vector3.forward * range;
Handles.DrawLine(Vector3.zero, rangeCenter);
// Drawing the outer Cone and also z-testing it to draw another color if behind
Handles.color = outerColor;
DrawHandleDirections = HandleDirections.Left | HandleDirections.Right;
DrawConeWireframe(outerDiscRadius, outerDiscDistance, DrawHandleDirections);
// Bottom arcs, making a nice rounded shape
Handles.DrawWireArc(Vector3.zero, Vector3.right, vectorLineUp, outerAngle, range);
Handles.DrawWireArc(Vector3.zero, Vector3.up, vectorLineLeft, outerAngle, range);
// If we are using shadows we draw the near plane for shadows
if (spotlight.shadows != LightShadows.None && spotlight.lightmapBakeType != LightmapBakeType.Baked)
{
DrawShadowNearPlane(spotlight, innerColor);
}
}
static void DrawShadowNearPlane(Light spotlight, Color color)
{
Color previousColor = Handles.color;
Handles.color = color;
var shadowDiscRadius = Mathf.Tan(spotlight.spotAngle * Mathf.Deg2Rad * 0.5f) * spotlight.shadowNearPlane;
var shadowDiscDistance = spotlight.shadowNearPlane;
Handles.DrawWireDisc(Vector3.forward * shadowDiscDistance, Vector3.forward, shadowDiscRadius);
Handles.DrawLine(Vector3.forward * shadowDiscDistance, (Vector3.right * shadowDiscRadius) + (Vector3.forward * shadowDiscDistance));
Handles.DrawLine(Vector3.forward * shadowDiscDistance, (-Vector3.right * shadowDiscRadius) + (Vector3.forward * shadowDiscDistance));
Handles.color = previousColor;
}
static void DrawConeWireframe(float radius, float height, HandleDirections handleDirections)
{
var rangeCenter = Vector3.forward * height;
if (handleDirections.HasFlag(HandleDirections.Up))
{
var rangeUp = rangeCenter + Vector3.up * radius;
Handles.DrawLine(Vector3.zero, rangeUp);
}
if (handleDirections.HasFlag(HandleDirections.Down))
{
var rangeDown = rangeCenter - Vector3.up * radius;
Handles.DrawLine(Vector3.zero, rangeDown);
}
if (handleDirections.HasFlag(HandleDirections.Right))
{
var rangeRight = rangeCenter + Vector3.right * radius;
Handles.DrawLine(Vector3.zero, rangeRight);
}
if (handleDirections.HasFlag(HandleDirections.Left))
{
var rangeLeft = rangeCenter - Vector3.right * radius;
Handles.DrawLine(Vector3.zero, rangeLeft);
}
//Draw Circle
Handles.DrawWireDisc(rangeCenter, Vector3.forward, radius);
}
static float DrawConeHandles(Vector3 position, float angle, float range, HandleDirections handleDirections, string controlName)
{
if (handleDirections.HasFlag(HandleDirections.Left))
{
angle = SizeSliderSpotAngle(position, Vector3.forward, -Vector3.right, range, angle, controlName);
}
if (handleDirections.HasFlag(HandleDirections.Up))
{
angle = SizeSliderSpotAngle(position, Vector3.forward, Vector3.up, range, angle, controlName);
}
if (handleDirections.HasFlag(HandleDirections.Right))
{
angle = SizeSliderSpotAngle(position, Vector3.forward, Vector3.right, range, angle, controlName);
}
if (handleDirections.HasFlag(HandleDirections.Down))
{
angle = SizeSliderSpotAngle(position, Vector3.forward, -Vector3.up, range, angle, controlName);
}
return angle;
}
static float SliderLineHandle(int id, Vector3 position, Vector3 direction, float value, string labelText = "")
{
Vector3 pos = position + direction * value;
float sizeHandle = HandleUtility.GetHandleSize(pos);
bool temp = GUI.changed;
GUI.changed = false;
pos = Handles.Slider(id, pos, direction, sizeHandle * 0.03f, Handles.DotHandleCap, 0f);
if (GUI.changed)
{
value = Vector3.Dot(pos - position, direction);
}
GUI.changed |= temp;
if (GUIUtility.hotControl == id && !String.IsNullOrEmpty(labelText))
{
labelText += FormattableString.Invariant($"{value:0.00}");
DrawHandleLabel(pos, labelText);
}
return value;
}
static float SizeSliderSpotAngle(Vector3 position, Vector3 forward, Vector3 axis, float range, float spotAngle, string controlName)
{
if (Math.Abs(spotAngle) <= 0.05f)
return spotAngle;
var angledForward = Quaternion.AngleAxis(Mathf.Max(spotAngle, 0.05f) * 0.5f, axis) * forward;
var centerToLeftOnSphere = (angledForward * range + position) - (position + forward * range);
bool temp = GUI.changed;
GUI.changed = false;
var handlePosition = position + forward * range;
var id = GUIUtility.GetControlID(FocusType.Passive);
var newMagnitude = Mathf.Max(0f, SliderLineHandle(id, handlePosition, centerToLeftOnSphere.normalized, centerToLeftOnSphere.magnitude));
if (GUI.changed)
{
centerToLeftOnSphere = centerToLeftOnSphere.normalized * newMagnitude;
angledForward = (centerToLeftOnSphere + (position + forward * range) - position).normalized;
spotAngle = Mathf.Clamp(Mathf.Acos(Vector3.Dot(forward, angledForward)) * Mathf.Rad2Deg * 2, 0f, 179f);
if (spotAngle <= 0.05f || float.IsNaN(spotAngle))
spotAngle = 0f;
}
GUI.changed |= temp;
if (GUIUtility.hotControl == id)
{
var pos = handlePosition + centerToLeftOnSphere.normalized * newMagnitude;
string labelText = FormattableString.Invariant($"{controlName} {spotAngle:0.00}");
DrawHandleLabel(pos, labelText);
}
return spotAngle;
}
}
}

View File

@@ -0,0 +1,448 @@
using System.IO;
using Unity.Collections;
using UnityEditor;
#if UNITY_2020_2_OR_NEWER
using UnityEditor.AssetImporters;
#else
using UnityEditor.Experimental.AssetImporters;
#endif
using UnityEngine;
namespace UnityEditor.Rendering
{
// Photometric type coordinate system references:
// https://www.ies.org/product/approved-method-guide-to-goniometer-measurements-and-types-and-photometric-coordinate-systems/
// https://support.agi32.com/support/solutions/articles/22000209748-type-a-type-b-and-type-c-photometry
/// <summary>
/// IES class which is common for the Importers
/// </summary>
[System.Serializable]
public class IESEngine
{
const float k_HalfPi = 0.5f * Mathf.PI;
const float k_TwoPi = 2.0f * Mathf.PI;
internal IESReader m_iesReader = new IESReader();
internal string FileFormatVersion { get => m_iesReader.FileFormatVersion; }
internal TextureImporterType m_TextureGenerationType = TextureImporterType.Cookie;
/// <summary>
/// setter for the Texture generation Type
/// </summary>
public TextureImporterType TextureGenerationType
{
set { m_TextureGenerationType = value; }
}
/// <summary>
/// Method to read the IES File
/// </summary>
/// <param name="iesFilePath">Path to the IES file in the Disk.</param>
/// <returns>An error message or warning otherwise null if no error</returns>
public string ReadFile(string iesFilePath)
{
if (!File.Exists(iesFilePath))
{
return "IES file does not exist.";
}
string errorMessage;
try
{
errorMessage = m_iesReader.ReadFile(iesFilePath);
}
catch (IOException ioEx)
{
return ioEx.Message;
}
return errorMessage;
}
/// <summary>
/// Check a keyword
/// </summary>
/// <param name="keyword">A keyword to check if exist.</param>
/// <returns>A Keyword if exist inside the internal Dictionary</returns>
public string GetKeywordValue(string keyword)
{
return m_iesReader.GetKeywordValue(keyword);
}
/// <summary>
/// Getter (as a string) for the Photometric Type
/// </summary>
/// <returns>The current Photometric Type</returns>
public string GetPhotometricType()
{
switch (m_iesReader.PhotometricType)
{
case 3: // type A
return "Type A";
case 2: // type B
return "Type B";
default: // type C
return "Type C";
}
}
/// <summary>
/// Get the CUrrent Max intensity
/// </summary>
/// <returns>A pair of the intensity follow by the used unit (candelas or lumens)</returns>
public (float, string) GetMaximumIntensity()
{
if (m_iesReader.TotalLumens == -1f) // absolute photometry
{
return (m_iesReader.MaxCandelas, "Candelas");
}
else
{
return (m_iesReader.TotalLumens, "Lumens");
}
}
/// <summary>
/// Generated a Cube texture based on the internal PhotometricType
/// </summary>
/// <param name="compression">Compression parameter requestted.</param>
/// <param name="textureSize">The resquested size.</param>
/// <returns>A Cubemap representing this IES</returns>
public (string, Texture) GenerateCubeCookie(TextureImporterCompression compression, int textureSize)
{
int width = 2 * textureSize;
int height = 2 * textureSize;
NativeArray<Color32> colorBuffer;
switch (m_iesReader.PhotometricType)
{
case 3: // type A
colorBuffer = BuildTypeACylindricalTexture(width, height);
break;
case 2: // type B
colorBuffer = BuildTypeBCylindricalTexture(width, height);
break;
default: // type C
colorBuffer = BuildTypeCCylindricalTexture(width, height);
break;
}
return GenerateTexture(m_TextureGenerationType, TextureImporterShape.TextureCube, compression, width, height, colorBuffer);
}
// Gnomonic projection reference:
// http://speleotrove.com/pangazer/gnomonic_projection.html
/// <summary>
/// Generating a 2D Texture of this cookie, using a Gnomonic projection of the bottom of the IES
/// </summary>
/// <param name="compression">Compression parameter requestted.</param>
/// <param name="coneAngle">Cone angle used to performe the Gnomonic projection.</param>
/// <param name="textureSize">The resquested size.</param>
/// <param name="applyLightAttenuation">Bool to enable or not the Light Attenuation based on the squared distance.</param>
/// <returns>A Generated 2D texture doing the projection of the IES using the Gnomonic projection of the bottom half hemisphere with the given 'cone angle'</returns>
public (string, Texture) Generate2DCookie(TextureImporterCompression compression, float coneAngle, int textureSize, bool applyLightAttenuation)
{
NativeArray<Color32> colorBuffer;
switch (m_iesReader.PhotometricType)
{
case 3: // type A
colorBuffer = BuildTypeAGnomonicTexture(coneAngle, textureSize, applyLightAttenuation);
break;
case 2: // type B
colorBuffer = BuildTypeBGnomonicTexture(coneAngle, textureSize, applyLightAttenuation);
break;
default: // type C
colorBuffer = BuildTypeCGnomonicTexture(coneAngle, textureSize, applyLightAttenuation);
break;
}
return GenerateTexture(m_TextureGenerationType, TextureImporterShape.Texture2D, compression, textureSize, textureSize, colorBuffer);
}
private (string, Texture) GenerateCylindricalTexture(TextureImporterCompression compression, int textureSize)
{
int width = 2 * textureSize;
int height = textureSize;
NativeArray<Color32> colorBuffer;
switch (m_iesReader.PhotometricType)
{
case 3: // type A
colorBuffer = BuildTypeACylindricalTexture(width, height);
break;
case 2: // type B
colorBuffer = BuildTypeBCylindricalTexture(width, height);
break;
default: // type C
colorBuffer = BuildTypeCCylindricalTexture(width, height);
break;
}
return GenerateTexture(TextureImporterType.Default, TextureImporterShape.Texture2D, compression, width, height, colorBuffer);
}
(string, Texture) GenerateTexture(TextureImporterType type, TextureImporterShape shape, TextureImporterCompression compression, int width, int height, NativeArray<Color32> colorBuffer)
{
// Default values set by the TextureGenerationSettings constructor can be found in this file on GitHub:
// https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/AssetPipeline/TextureGenerator.bindings.cs
var settings = new TextureGenerationSettings(type);
SourceTextureInformation textureInfo = settings.sourceTextureInformation;
textureInfo.containsAlpha = true;
textureInfo.height = height;
textureInfo.width = width;
TextureImporterSettings textureImporterSettings = settings.textureImporterSettings;
textureImporterSettings.alphaSource = TextureImporterAlphaSource.FromInput;
textureImporterSettings.aniso = 0;
textureImporterSettings.borderMipmap = (textureImporterSettings.textureType == TextureImporterType.Cookie);
textureImporterSettings.filterMode = FilterMode.Bilinear;
textureImporterSettings.generateCubemap = TextureImporterGenerateCubemap.Cylindrical;
textureImporterSettings.mipmapEnabled = false;
textureImporterSettings.npotScale = TextureImporterNPOTScale.None;
textureImporterSettings.readable = true;
textureImporterSettings.sRGBTexture = false;
textureImporterSettings.textureShape = shape;
textureImporterSettings.wrapMode = textureImporterSettings.wrapModeU = textureImporterSettings.wrapModeV = textureImporterSettings.wrapModeW = TextureWrapMode.Clamp;
TextureImporterPlatformSettings platformSettings = settings.platformSettings;
platformSettings.maxTextureSize = 2048;
platformSettings.resizeAlgorithm = TextureResizeAlgorithm.Bilinear;
platformSettings.textureCompression = compression;
TextureGenerationOutput output = TextureGenerator.GenerateTexture(settings, colorBuffer);
if (output.importWarnings.Length > 0)
{
Debug.LogWarning("Cannot properly generate IES texture:\n" + string.Join("\n", output.importWarnings));
}
return (output.importInspectorWarnings, output.texture);
}
NativeArray<Color32> BuildTypeACylindricalTexture(int width, int height)
{
float stepU = 360f / (width - 1);
float stepV = 180f / (height - 1);
var textureBuffer = new NativeArray<Color32>(width * height, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
for (int y = 0; y < height; y++)
{
var slice = new NativeSlice<Color32>(textureBuffer, y * width, width);
float latitude = y * stepV - 90f; // in range [-90..+90] degrees
float verticalAnglePosition = m_iesReader.ComputeVerticalAnglePosition(latitude);
for (int x = 0; x < width; x++)
{
float longitude = x * stepU - 180f; // in range [-180..+180] degrees
float horizontalAnglePosition = m_iesReader.ComputeTypeAorBHorizontalAnglePosition(longitude);
byte value = (byte)((m_iesReader.InterpolateBilinear(horizontalAnglePosition, verticalAnglePosition) / m_iesReader.MaxCandelas) * 255);
slice[x] = new Color32(value, value, value, value);
}
}
return textureBuffer;
}
NativeArray<Color32> BuildTypeBCylindricalTexture(int width, int height)
{
float stepU = k_TwoPi / (width - 1);
float stepV = Mathf.PI / (height - 1);
var textureBuffer = new NativeArray<Color32>(width * height, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
for (int y = 0; y < height; y++)
{
var slice = new NativeSlice<Color32>(textureBuffer, y * width, width);
float v = y * stepV - k_HalfPi; // in range [-90..+90] degrees
float sinV = Mathf.Sin(v);
float cosV = Mathf.Cos(v);
for (int x = 0; x < width; x++)
{
float u = Mathf.PI - x * stepU; // in range [+180..-180] degrees
float sinU = Mathf.Sin(u);
float cosU = Mathf.Cos(u);
// Since a type B luminaire is turned on its side, rotate it to make its polar axis horizontal.
float longitude = Mathf.Atan2(sinV, cosU * cosV) * Mathf.Rad2Deg; // in range [-180..+180] degrees
float latitude = Mathf.Asin(-sinU * cosV) * Mathf.Rad2Deg; // in range [-90..+90] degrees
float horizontalAnglePosition = m_iesReader.ComputeTypeAorBHorizontalAnglePosition(longitude);
float verticalAnglePosition = m_iesReader.ComputeVerticalAnglePosition(latitude);
byte value = (byte)((m_iesReader.InterpolateBilinear(horizontalAnglePosition, verticalAnglePosition) / m_iesReader.MaxCandelas) * 255);
slice[x] = new Color32(value, value, value, value);
}
}
return textureBuffer;
}
NativeArray<Color32> BuildTypeCCylindricalTexture(int width, int height)
{
float stepU = k_TwoPi / (width - 1);
float stepV = Mathf.PI / (height - 1);
var textureBuffer = new NativeArray<Color32>(width * height, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
for (int y = 0; y < height; y++)
{
var slice = new NativeSlice<Color32>(textureBuffer, y * width, width);
float v = y * stepV - k_HalfPi; // in range [-90..+90] degrees
float sinV = Mathf.Sin(v);
float cosV = Mathf.Cos(v);
for (int x = 0; x < width; x++)
{
float u = Mathf.PI - x * stepU; // in range [+180..-180] degrees
float sinU = Mathf.Sin(u);
float cosU = Mathf.Cos(u);
// Since a type C luminaire is generally aimed at nadir, orient it toward +Z at the center of the cylindrical texture.
float longitude = ((Mathf.Atan2(sinU * cosV, sinV) + k_TwoPi) % k_TwoPi) * Mathf.Rad2Deg; // in range [0..360] degrees
float latitude = (Mathf.Asin(-cosU * cosV) + k_HalfPi) * Mathf.Rad2Deg; // in range [0..180] degrees
float horizontalAnglePosition = m_iesReader.ComputeTypeCHorizontalAnglePosition(longitude);
float verticalAnglePosition = m_iesReader.ComputeVerticalAnglePosition(latitude);
byte value = (byte)((m_iesReader.InterpolateBilinear(horizontalAnglePosition, verticalAnglePosition) / m_iesReader.MaxCandelas) * 255);
slice[x] = new Color32(value, value, value, value);
}
}
return textureBuffer;
}
NativeArray<Color32> BuildTypeAGnomonicTexture(float coneAngle, int size, bool applyLightAttenuation)
{
float limitUV = Mathf.Tan(0.5f * coneAngle * Mathf.Deg2Rad);
float stepUV = (2 * limitUV) / (size - 3);
var textureBuffer = new NativeArray<Color32>(size * size, Allocator.Temp, NativeArrayOptions.ClearMemory);
// Leave a one-pixel black border around the texture to avoid cookie spilling.
for (int y = 1; y < size - 1; y++)
{
var slice = new NativeSlice<Color32>(textureBuffer, y * size, size);
float v = (y - 1) * stepUV - limitUV;
for (int x = 1; x < size - 1; x++)
{
float u = (x - 1) * stepUV - limitUV;
float rayLengthSquared = u * u + v * v + 1;
float longitude = Mathf.Atan(u) * Mathf.Rad2Deg; // in range [-90..+90] degrees
float latitude = Mathf.Asin(v / Mathf.Sqrt(rayLengthSquared)) * Mathf.Rad2Deg; // in range [-90..+90] degrees
float horizontalAnglePosition = m_iesReader.ComputeTypeCHorizontalAnglePosition(longitude);
float verticalAnglePosition = m_iesReader.ComputeVerticalAnglePosition(latitude);
// Factor in the light attenuation further from the texture center.
float lightAttenuation = applyLightAttenuation ? rayLengthSquared : 1f;
byte value = (byte)((m_iesReader.InterpolateBilinear(horizontalAnglePosition, verticalAnglePosition) / (m_iesReader.MaxCandelas * lightAttenuation)) * 255);
slice[x] = new Color32(value, value, value, value);
}
}
return textureBuffer;
}
NativeArray<Color32> BuildTypeBGnomonicTexture(float coneAngle, int size, bool applyLightAttenuation)
{
float limitUV = Mathf.Tan(0.5f * coneAngle * Mathf.Deg2Rad);
float stepUV = (2 * limitUV) / (size - 3);
var textureBuffer = new NativeArray<Color32>(size * size, Allocator.Temp, NativeArrayOptions.ClearMemory);
// Leave a one-pixel black border around the texture to avoid cookie spilling.
for (int y = 1; y < size - 1; y++)
{
var slice = new NativeSlice<Color32>(textureBuffer, y * size, size);
float v = (y - 1) * stepUV - limitUV;
for (int x = 1; x < size - 1; x++)
{
float u = (x - 1) * stepUV - limitUV;
float rayLengthSquared = u * u + v * v + 1;
// Since a type B luminaire is turned on its side, U and V are flipped.
float longitude = Mathf.Atan(v) * Mathf.Rad2Deg; // in range [-90..+90] degrees
float latitude = Mathf.Asin(u / Mathf.Sqrt(rayLengthSquared)) * Mathf.Rad2Deg; // in range [-90..+90] degrees
float horizontalAnglePosition = m_iesReader.ComputeTypeCHorizontalAnglePosition(longitude);
float verticalAnglePosition = m_iesReader.ComputeVerticalAnglePosition(latitude);
// Factor in the light attenuation further from the texture center.
float lightAttenuation = applyLightAttenuation ? rayLengthSquared : 1f;
byte value = (byte)((m_iesReader.InterpolateBilinear(horizontalAnglePosition, verticalAnglePosition) / (m_iesReader.MaxCandelas * lightAttenuation)) * 255);
slice[x] = new Color32(value, value, value, value);
}
}
return textureBuffer;
}
NativeArray<Color32> BuildTypeCGnomonicTexture(float coneAngle, int size, bool applyLightAttenuation)
{
float limitUV = Mathf.Tan(0.5f * coneAngle * Mathf.Deg2Rad);
float stepUV = (2 * limitUV) / (size - 3);
var textureBuffer = new NativeArray<Color32>(size * size, Allocator.Temp, NativeArrayOptions.ClearMemory);
// Leave a one-pixel black border around the texture to avoid cookie spilling.
for (int y = 1; y < size - 1; y++)
{
var slice = new NativeSlice<Color32>(textureBuffer, y * size, size);
float v = (y - 1) * stepUV - limitUV;
for (int x = 1; x < size - 1; x++)
{
float u = (x - 1) * stepUV - limitUV;
float uvLength = Mathf.Sqrt(u * u + v * v);
float longitude = ((Mathf.Atan2(v, u) - k_HalfPi + k_TwoPi) % k_TwoPi) * Mathf.Rad2Deg; // in range [0..360] degrees
float latitude = Mathf.Atan(uvLength) * Mathf.Rad2Deg; // in range [0..90] degrees
float horizontalAnglePosition = m_iesReader.ComputeTypeCHorizontalAnglePosition(longitude);
float verticalAnglePosition = m_iesReader.ComputeVerticalAnglePosition(latitude);
// Factor in the light attenuation further from the texture center.
float lightAttenuation = applyLightAttenuation ? (uvLength * uvLength + 1) : 1f;
byte value = (byte)((m_iesReader.InterpolateBilinear(horizontalAnglePosition, verticalAnglePosition) / (m_iesReader.MaxCandelas * lightAttenuation)) * 255);
slice[x] = new Color32(value, value, value, value);
}
}
return textureBuffer;
}
}
}

View File

@@ -0,0 +1,113 @@
using System.IO;
using UnityEditor;
using UnityEngine;
#if UNITY_2020_2_OR_NEWER
using UnityEditor.AssetImporters;
#else
using UnityEditor.Experimental.AssetImporters;
#endif
namespace UnityEditor.Rendering
{
/// <summary>
/// Common class use to share code between implementation of IES Importeres
/// </summary>
[System.Serializable]
[ScriptedImporter(1, "ies")]
public partial class IESImporter : ScriptedImporter
{
/// <summary>
/// IES Engine
/// </summary>
public IESEngine engine = new IESEngine();
/// <summary>
/// IES Meta data stored in the ies file
/// </summary>
public IESMetaData iesMetaData = new IESMetaData();
/// <summary>
/// Delegate prototype which will be sent by the pipeline implementation of the IES Importer
/// Must be initialized during the creation of the SRP
/// </summary>
public static event System.Action<AssetImportContext, string, bool, string, float, Light, Texture> createRenderPipelinePrefabLight;
/// <summary>
/// Common method performing the import of the asset
/// </summary>
/// <param name="ctx">Asset importer context.</param>
public override void OnImportAsset(AssetImportContext ctx)
{
engine.TextureGenerationType = TextureImporterType.Default;
Texture cookieTextureCube = null;
Texture cookieTexture2D = null;
string iesFilePath = Path.Combine(Path.GetDirectoryName(Application.dataPath), ctx.assetPath);
string errorMessage = engine.ReadFile(iesFilePath);
if (string.IsNullOrEmpty(errorMessage))
{
iesMetaData.FileFormatVersion = engine.FileFormatVersion;
iesMetaData.IESPhotometricType = engine.GetPhotometricType();
iesMetaData.Manufacturer = engine.GetKeywordValue("MANUFAC");
iesMetaData.LuminaireCatalogNumber = engine.GetKeywordValue("LUMCAT");
iesMetaData.LuminaireDescription = engine.GetKeywordValue("LUMINAIRE");
iesMetaData.LampCatalogNumber = engine.GetKeywordValue("LAMPCAT");
iesMetaData.LampDescription = engine.GetKeywordValue("LAMP");
(iesMetaData.IESMaximumIntensity, iesMetaData.IESMaximumIntensityUnit) = engine.GetMaximumIntensity();
string warningMessage;
(warningMessage, cookieTextureCube) = engine.GenerateCubeCookie(iesMetaData.CookieCompression, (int)iesMetaData.iesSize);
if (!string.IsNullOrEmpty(warningMessage))
{
ctx.LogImportWarning($"Cannot properly generate IES Cube texture: {warningMessage}");
}
cookieTextureCube.IncrementUpdateCount();
(warningMessage, cookieTexture2D) = engine.Generate2DCookie(iesMetaData.CookieCompression, iesMetaData.SpotAngle, (int)iesMetaData.iesSize, iesMetaData.ApplyLightAttenuation);
if (!string.IsNullOrEmpty(warningMessage))
{
ctx.LogImportWarning($"Cannot properly generate IES 2D texture: {warningMessage}");
}
cookieTexture2D.IncrementUpdateCount();
}
else
{
ctx.LogImportError($"Cannot read IES file '{iesFilePath}': {errorMessage}");
}
string iesFileName = Path.GetFileNameWithoutExtension(ctx.assetPath);
var iesObject = ScriptableObject.CreateInstance<IESObject>();
iesObject.iesMetaData = iesMetaData;
GameObject lightObject = new GameObject(iesFileName);
lightObject.transform.localEulerAngles = new Vector3(90f, 0f, iesMetaData.LightAimAxisRotation);
Light light = lightObject.AddComponent<Light>();
light.type = (iesMetaData.PrefabLightType == IESLightType.Point) ? LightType.Point : LightType.Spot;
light.intensity = 1f; // would need a better intensity value formula
light.range = 10f; // would need a better range value formula
light.spotAngle = iesMetaData.SpotAngle;
ctx.AddObjectToAsset("IES", iesObject);
ctx.SetMainObject(iesObject);
IESImporter.createRenderPipelinePrefabLight?.Invoke(ctx, iesFileName, iesMetaData.UseIESMaximumIntensity, iesMetaData.IESMaximumIntensityUnit, iesMetaData.IESMaximumIntensity, light, (iesMetaData.PrefabLightType == IESLightType.Point) ? cookieTextureCube : cookieTexture2D);
if (cookieTextureCube != null)
{
cookieTextureCube.name = iesFileName + "-Cube-IES";
ctx.AddObjectToAsset(cookieTextureCube.name, cookieTextureCube);
}
if (cookieTexture2D != null)
{
cookieTexture2D.name = iesFileName + "-2D-IES";
ctx.AddObjectToAsset(cookieTexture2D.name, cookieTexture2D);
}
}
}
}

View File

@@ -0,0 +1,332 @@
using System.Reflection;
using UnityEditor;
#if UNITY_2020_2_OR_NEWER
using UnityEditor.AssetImporters;
#else
using UnityEditor.Experimental.AssetImporters;
#endif
using UnityEngine;
using UnityEngine.Rendering;
namespace UnityEditor.Rendering
{
/// <summary>
/// Common class for IES Importer Editor (currently implemented only on HDRP)
/// </summary>
public class IESImporterEditor
{
GUIStyle m_WordWrapStyle = new GUIStyle();
SerializedProperty m_FileFormatVersionProp;
SerializedProperty m_IESPhotometricTypeProp;
SerializedProperty m_IESMaximumIntensityProp;
SerializedProperty m_IESMaximumIntensityUnitProp;
SerializedProperty m_ManufacturerProp;
SerializedProperty m_LuminaireCatalogNumberProp;
SerializedProperty m_LuminaireDescriptionProp;
SerializedProperty m_LampCatalogNumberProp;
SerializedProperty m_LampDescriptionProp;
SerializedProperty m_PrefabLightTypeProp;
SerializedProperty m_SpotAngleProp;
SerializedProperty m_IESSizeProp;
SerializedProperty m_ApplyLightAttenuationProp;
SerializedProperty m_UseIESMaximumIntensityProp;
SerializedProperty m_CookieCompressionProp;
/// <summary>
/// Property to the aim axis rotation for projection
/// </summary>
protected SerializedProperty m_LightAimAxisRotationProp;
bool m_ShowLuminaireProductInformation = true;
bool m_ShowLightProperties = true;
/// <summary>
/// Object used to setup Preview renderer
/// </summary>
protected PreviewRenderUtility m_PreviewRenderUtility = null;
/// <summary>
/// Delegate prototype sent by the specialization of the IESImporterEditor per Render Pipeline
/// </summary>
public delegate void LayoutRenderPipelineUseIesMaximumIntensity();
/// <summary>
/// Delegate prototype sent by the specialization of the IESImporterEditor per Render Pipeline
/// </summary>
/// <param name="camera">Current camera</param>
public delegate void SetupRenderPipelinePreviewCamera(Camera camera);
/// <summary>
/// Delegate prototype sent by the specialization of the IESImporterEditor per Render Pipeline
/// </summary>
/// <param name="light">Light will be setuped, specialization for a given SRP</param>
public delegate void SetupRenderPipelinePreviewLight(Light light);
/// <summary>
/// Delegate prototype sent by the specialization of the IESImporterEditor per Render Pipeline
/// </summary>
/// <param name="wallRenderer">Setup a wall for the preview</param>
public delegate void SetupRenderPipelinePreviewWallRenderer(MeshRenderer wallRenderer);
/// <summary>
/// Delegate prototype sent by the specialization of the IESImporterEditor per Render Pipeline
/// </summary>
/// <param name="floorRenderer">Setup a floor for the preview</param>
public delegate void SetupRenderPipelinePreviewFloorRenderer(MeshRenderer floorRenderer);
/// <summary>
/// Delegate prototype sent by the specialization of the IESImporterEditor per Render Pipeline
/// </summary>
/// <param name="light">Light used to setup the IES</param>
/// <param name="useIESMaximumIntensityProp">Serialized property to the "useIESMaximumIntensity" property</param>
/// <param name="iesMaximumIntensityUnitProp">Serialized property to the "iesMaximumIntensityUnit" property</param>
/// <param name="iesMaximumIntensityProp">Serialized property to the "iesMaximumIntensity" property</param>
public delegate void SetupRenderPipelinePreviewLightIntensity(Light light, SerializedProperty useIESMaximumIntensityProp, SerializedProperty iesMaximumIntensityUnitProp, SerializedProperty iesMaximumIntensityProp);
/// <summary>
/// Callback called on the Implemented IESImporterEditor (currently on HDRP Only)
/// </summary>
/// <param name="serializedObject">Serialized object which can be linked to IESMetadata</param>
public void CommonOnEnable(SerializedProperty serializedObject)
{
m_WordWrapStyle.wordWrap = true;
m_FileFormatVersionProp = serializedObject.FindPropertyRelative("FileFormatVersion");
m_IESPhotometricTypeProp = serializedObject.FindPropertyRelative("IESPhotometricType");
m_IESMaximumIntensityProp = serializedObject.FindPropertyRelative("IESMaximumIntensity");
m_IESMaximumIntensityUnitProp = serializedObject.FindPropertyRelative("IESMaximumIntensityUnit");
m_ManufacturerProp = serializedObject.FindPropertyRelative("Manufacturer");
m_LuminaireCatalogNumberProp = serializedObject.FindPropertyRelative("LuminaireCatalogNumber");
m_LuminaireDescriptionProp = serializedObject.FindPropertyRelative("LuminaireDescription");
m_LampCatalogNumberProp = serializedObject.FindPropertyRelative("LampCatalogNumber");
m_LampDescriptionProp = serializedObject.FindPropertyRelative("LampDescription");
m_PrefabLightTypeProp = serializedObject.FindPropertyRelative("PrefabLightType");
m_SpotAngleProp = serializedObject.FindPropertyRelative("SpotAngle");
m_IESSizeProp = serializedObject.FindPropertyRelative("iesSize");
m_ApplyLightAttenuationProp = serializedObject.FindPropertyRelative("ApplyLightAttenuation");
m_UseIESMaximumIntensityProp = serializedObject.FindPropertyRelative("UseIESMaximumIntensity");
m_CookieCompressionProp = serializedObject.FindPropertyRelative("CookieCompression");
m_LightAimAxisRotationProp = serializedObject.FindPropertyRelative("LightAimAxisRotation");
}
/// <summary>
/// Callback called on the Implemented IESImporterEditor (currently on HDRP Only)
/// </summary>
/// <param name="scriptedImporter">The current specialized scripted importer using the common code</param>
public void CommonOnInspectorGUI(ScriptedImporterEditor scriptedImporter)
{
scriptedImporter.serializedObject.Update();
EditorGUILayout.LabelField("File Format Version", m_FileFormatVersionProp.stringValue);
EditorGUILayout.LabelField("Photometric Type", m_IESPhotometricTypeProp.stringValue);
EditorGUILayout.LabelField("Maximum Intensity", $"{m_IESMaximumIntensityProp.floatValue} {m_IESMaximumIntensityUnitProp.stringValue}");
if (m_ShowLuminaireProductInformation = EditorGUILayout.Foldout(m_ShowLuminaireProductInformation, "Luminaire Product Information"))
{
EditorGUILayout.LabelField(m_ManufacturerProp.displayName, m_ManufacturerProp.stringValue, m_WordWrapStyle);
EditorGUILayout.LabelField(m_LuminaireCatalogNumberProp.displayName, m_LuminaireCatalogNumberProp.stringValue, m_WordWrapStyle);
EditorGUILayout.LabelField(m_LuminaireDescriptionProp.displayName, m_LuminaireDescriptionProp.stringValue, m_WordWrapStyle);
EditorGUILayout.LabelField(m_LampCatalogNumberProp.displayName, m_LampCatalogNumberProp.stringValue, m_WordWrapStyle);
EditorGUILayout.LabelField(m_LampDescriptionProp.displayName, m_LampDescriptionProp.stringValue, m_WordWrapStyle);
}
if (m_ShowLightProperties = EditorGUILayout.Foldout(m_ShowLightProperties, "Light and Cookie Properties"))
{
EditorGUILayout.PropertyField(m_PrefabLightTypeProp, new GUIContent("Light Type"));
EditorGUILayout.PropertyField(m_SpotAngleProp);
EditorGUILayout.PropertyField(m_IESSizeProp, new GUIContent("IES Size"));
EditorGUILayout.PropertyField(m_ApplyLightAttenuationProp);
EditorGUILayout.PropertyField(m_CookieCompressionProp, new GUIContent("IES Compression"));
// Before enabling this feature, more experimentation is needed with the addition of a Volume in the PreviewRenderUtility scene.
EditorGUILayout.PropertyField(m_UseIESMaximumIntensityProp, new GUIContent("Use IES Maximum Intensity"));
using (new EditorGUILayout.HorizontalScope())
{
EditorGUILayout.PropertyField(m_LightAimAxisRotationProp, new GUIContent("Aim Axis Rotation"));
if (GUILayout.Button("Reset", GUILayout.Width(44)))
{
m_LightAimAxisRotationProp.floatValue = -90f;
}
}
}
scriptedImporter.serializedObject.ApplyModifiedProperties();
}
/// <summary>
/// Callback called on the Implemented IESImporterEditor (currently on HDRP Only)
/// </summary>
public void CommonApply()
{
if (m_PreviewRenderUtility != null)
{
m_PreviewRenderUtility.Cleanup();
m_PreviewRenderUtility = null;
}
}
/// <summary>
/// Callback called on the Implemented IESImporterEditor (currently on HDRP Only)
/// </summary>
/// <param name="setupRenderPipelinePreviewCamera">Delegate provided by the Render pipeline to setup the Preview Camera</param>
/// <param name="setupRenderPipelinePreviewFloorRenderer">Delegate provided by the Render pipeline to setup the Preview Light</param>
/// <param name="setupRenderPipelinePreviewLight">Delegate provided by the Render pipeline to setup the Preview Wall</param>
/// <param name="setupRenderPipelinePreviewWallRenderer">Delegate provided by the Render pipeline to setup the Preview Floor</param>
/// <returns>true to specified IES has a Preview</returns>
public bool CommonHasPreviewGUI(SetupRenderPipelinePreviewCamera setupRenderPipelinePreviewCamera,
SetupRenderPipelinePreviewLight setupRenderPipelinePreviewLight,
SetupRenderPipelinePreviewWallRenderer setupRenderPipelinePreviewWallRenderer,
SetupRenderPipelinePreviewFloorRenderer setupRenderPipelinePreviewFloorRenderer)
{
if (m_PreviewRenderUtility == null)
{
m_PreviewRenderUtility = new PreviewRenderUtility();
m_PreviewRenderUtility.ambientColor = Color.black;
m_PreviewRenderUtility.camera.fieldOfView = 60f;
m_PreviewRenderUtility.camera.nearClipPlane = 0.1f;
m_PreviewRenderUtility.camera.farClipPlane = 10f;
m_PreviewRenderUtility.camera.transform.localPosition = new Vector3(1.85f, 0.71f, 0f);
m_PreviewRenderUtility.camera.transform.localEulerAngles = new Vector3(15f, -90f, 0f);
setupRenderPipelinePreviewCamera(m_PreviewRenderUtility.camera);
m_PreviewRenderUtility.lights[0].type = (m_PrefabLightTypeProp.enumValueIndex == (int)IESLightType.Point) ? LightType.Point : LightType.Spot;
m_PreviewRenderUtility.lights[0].color = Color.white;
m_PreviewRenderUtility.lights[0].intensity = 1f;
m_PreviewRenderUtility.lights[0].range = 10f;
m_PreviewRenderUtility.lights[0].spotAngle = m_SpotAngleProp.floatValue;
m_PreviewRenderUtility.lights[0].transform.localPosition = new Vector3(0.14f, 1f, 0f);
m_PreviewRenderUtility.lights[0].transform.localEulerAngles = new Vector3(90f, 0f, -90f);
setupRenderPipelinePreviewLight(m_PreviewRenderUtility.lights[0]);
m_PreviewRenderUtility.lights[1].intensity = 0f;
GameObject previewWall = GameObject.CreatePrimitive(PrimitiveType.Plane);
previewWall.name = "IESPreviewWall";
previewWall.hideFlags = HideFlags.HideAndDontSave;
previewWall.transform.localPosition = new Vector3(0f, 4f, 0f);
previewWall.transform.localEulerAngles = new Vector3(0f, 0f, -90f);
previewWall.transform.localScale = new Vector3(1f, 1f, 10f);
MeshRenderer previewWallRenderer = previewWall.GetComponent<MeshRenderer>();
previewWallRenderer.lightProbeUsage = LightProbeUsage.Off;
previewWallRenderer.reflectionProbeUsage = ReflectionProbeUsage.Off;
previewWallRenderer.material = AssetDatabase.GetBuiltinExtraResource<Material>("Default-Material.mat");
setupRenderPipelinePreviewWallRenderer(previewWallRenderer);
m_PreviewRenderUtility.AddSingleGO(previewWall);
GameObject previewFloor = GameObject.CreatePrimitive(PrimitiveType.Plane);
previewFloor.name = "IESPreviewFloor";
previewFloor.hideFlags = HideFlags.HideAndDontSave;
previewFloor.transform.localPosition = new Vector3(4f, 0f, 0f);
previewFloor.transform.localEulerAngles = new Vector3(0f, 0f, 0f);
previewFloor.transform.localScale = new Vector3(1f, 1f, 10f);
MeshRenderer previewFloorRenderer = previewFloor.GetComponent<MeshRenderer>();
previewFloorRenderer.lightProbeUsage = LightProbeUsage.Off;
previewFloorRenderer.reflectionProbeUsage = ReflectionProbeUsage.Off;
previewFloorRenderer.material = AssetDatabase.GetBuiltinExtraResource<Material>("Default-Diffuse.mat");
setupRenderPipelinePreviewFloorRenderer(previewFloorRenderer);
m_PreviewRenderUtility.AddSingleGO(previewFloor);
}
return true;
}
/// <summary>
/// Callback called on the Implemented IESImporterEditor (currently on HDRP Only)
/// </summary>
/// <returns>The title of the Preview</returns>
public GUIContent CommonGetPreviewTitle()
{
return new GUIContent("IES Luminaire Profile");
}
/// <summary>
/// Callback called on the Implemented IESImporterEditor (currently on HDRP Only)
/// </summary>
/// <param name="background">Background of the Preview</param>
/// <param name="r">Rect of the Preview</param>
/// <param name="target">ScriptedImporter targeted</param>
/// <param name="setupRenderPipelinePreviewLightIntensity">Delegate provided by the Rendering Pipeline to setup the Light Intensity</param>
public void CommonOnPreviewGUI(Rect r, GUIStyle background, ScriptedImporter target,
SetupRenderPipelinePreviewLightIntensity setupRenderPipelinePreviewLightIntensity)
{
if (Event.current.type == EventType.Repaint)
{
Texture cookieTexture = null;
Texture previewTexture = null;
if (m_PrefabLightTypeProp.enumValueIndex == (int)IESLightType.Point)
{
foreach (var subAsset in AssetDatabase.LoadAllAssetRepresentationsAtPath(target.assetPath))
{
if (subAsset.name.EndsWith("-Cube-IES"))
{
cookieTexture = subAsset as Texture;
break;
}
}
}
else // LightType.Spot
{
foreach (var subAsset in AssetDatabase.LoadAllAssetRepresentationsAtPath(target.assetPath))
{
if (subAsset.name.EndsWith("-2D-IES"))
{
cookieTexture = subAsset as Texture;
break;
}
}
}
if (cookieTexture != null)
{
m_PreviewRenderUtility.lights[0].transform.localEulerAngles = new Vector3(90f, 0f, m_LightAimAxisRotationProp.floatValue);
setupRenderPipelinePreviewLightIntensity(m_PreviewRenderUtility.lights[0], m_UseIESMaximumIntensityProp, m_IESMaximumIntensityUnitProp, m_IESMaximumIntensityProp);
m_PreviewRenderUtility.lights[0].cookie = cookieTexture;
m_PreviewRenderUtility.lights[0].type = m_PrefabLightTypeProp.enumValueIndex == (int)IESLightType.Point ? LightType.Point : LightType.Spot;
m_PreviewRenderUtility.BeginPreview(r, background);
bool fog = RenderSettings.fog;
Unsupported.SetRenderSettingsUseFogNoDirty(false);
m_PreviewRenderUtility.camera.Render();
Unsupported.SetRenderSettingsUseFogNoDirty(fog);
previewTexture = m_PreviewRenderUtility.EndPreview();
}
if (previewTexture == null)
{
GUI.DrawTexture(r, Texture2D.blackTexture, ScaleMode.StretchToFill, false);
}
else
{
GUI.DrawTexture(r, previewTexture, ScaleMode.ScaleToFit, false);
}
}
}
/// <summary>
/// Callback called on the Implemented IESImporterEditor (currently on HDRP Only)
/// </summary>
public void CommonOnDisable()
{
if (m_PreviewRenderUtility != null)
{
m_PreviewRenderUtility.Cleanup();
m_PreviewRenderUtility = null;
}
}
}
}

View File

@@ -0,0 +1,171 @@
using System.IO;
using UnityEditor;
using UnityEngine;
namespace UnityEditor.Rendering
{
/// <summary>
/// Various possible type for IES, in HDRP for Rectangular light we use spot version
/// </summary>
public enum IESLightType
{
/// <summary>
/// Point for the IES
/// </summary>
Point,
/// <summary>
/// Spot for IES (compatible with Area Light)
/// </summary>
Spot,
}
/// <summary>
/// Possible values for the IES Size.
/// </summary>
public enum IESResolution
{
/// <summary>Size 16</summary>
IESResolution16 = 16,
/// <summary>Size 32</summary>
IESResolution32 = 32,
/// <summary>Size 64</summary>
IESResolution64 = 64,
/// <summary>Size 128</summary>
IESResolution128 = 128,
/// <summary>Size 256</summary>
IESResolution256 = 256,
/// <summary>Size 512</summary>
IESResolution512 = 512,
/// <summary>Size 1024</summary>
IESResolution1024 = 1024,
/// <summary>Size 2048</summary>
IESResolution2048 = 2048,
/// <summary>Size 4096</summary>
IESResolution4096 = 4096
}
/// <summary>
/// Common class to store metadata of an IES file
/// </summary>
[System.Serializable]
public class IESMetaData
{
/// <summary>
/// Version of the IES File
/// </summary>
public string FileFormatVersion;
/// <summary>
/// Total light intensity (in Lumens) stored on the file, usage of it is optional (through the prefab subasset inside the IESObject)
/// </summary>
public string IESPhotometricType;
/// <summary>
/// IES Max Intensity depends on the various information stored on the IES file
/// </summary>
public float IESMaximumIntensity;
/// <summary>
/// Unit used to measure the IESMaximumIntensity
/// </summary>
public string IESMaximumIntensityUnit;
// IES luminaire product information.
/// <summary>
/// Manufacturer of the current IES file
/// </summary>
public string Manufacturer; // IES keyword MANUFAC
/// <summary>
/// Luninaire Catalog Number
/// </summary>
public string LuminaireCatalogNumber; // IES keyword LUMCAT
/// <summary>
/// Luminaire Description
/// </summary>
public string LuminaireDescription; // IES keyword LUMINAIRE
/// <summary>
/// Lamp Catalog Number
/// </summary>
public string LampCatalogNumber; // IES keyword LAMPCAT
/// <summary>
/// Lamp Description
/// </summary>
public string LampDescription; // IES keyword LAMP
/// <summary>
/// Prefab Light Type (optional to generate the texture used by the renderer)
/// </summary>
public IESLightType PrefabLightType = IESLightType.Point;
/// <summary>
/// Spot angle used for the Gnomonic projection of the IES. This parameter will be responsible of the pixel footprint in the 2D Texture
/// https://en.wikipedia.org/wiki/Gnomonic_projection
/// </summary>
[Range(1f, 179f)]
public float SpotAngle = 120f;
/// <summary>
/// IES Size of the texture used (same parameter for Point and Spot)
/// </summary>
public IESResolution iesSize = IESResolution.IESResolution128;
/// <summary>
/// Enable attenuation used for Spot recommanded to be true, particulary with large angle of "SpotAngle" (cf. Gnomonic Projection)
/// </summary>
public bool ApplyLightAttenuation = true;
/// <summary>
/// Enable max intensity for the texture generation
/// </summary>
public bool UseIESMaximumIntensity = true;
/// <summary>
/// Compression used to generate the texture (CompressedHQ by default (BC7))
/// </summary>
public TextureImporterCompression CookieCompression = TextureImporterCompression.CompressedHQ;
/// <summary>
/// Internally we use 2D projection, we have to choose one axis to project the IES propertly
/// </summary>
[Range(-180f, 180f)]
public float LightAimAxisRotation = -90f;
/// <summary>
/// Get Hash describing an unique IES
/// </summary>
/// <returns>The Hash of the IES Object</returns>
public override int GetHashCode()
{
int hash = base.GetHashCode();
hash = hash * 23 + FileFormatVersion.GetHashCode();
hash = hash * 23 + IESPhotometricType.GetHashCode();
hash = hash * 23 + IESMaximumIntensity.GetHashCode();
hash = hash * 23 + IESMaximumIntensityUnit.GetHashCode();
hash = hash * 23 + Manufacturer.GetHashCode();
hash = hash * 23 + LuminaireCatalogNumber.GetHashCode();
hash = hash * 23 + LuminaireDescription.GetHashCode();
hash = hash * 23 + LampCatalogNumber.GetHashCode();
hash = hash * 23 + LampDescription.GetHashCode();
hash = hash * 23 + PrefabLightType.GetHashCode();
hash = hash * 23 + SpotAngle.GetHashCode();
hash = hash * 23 + iesSize.GetHashCode();
hash = hash * 23 + ApplyLightAttenuation.GetHashCode();
hash = hash * 23 + UseIESMaximumIntensity.GetHashCode();
return hash;
}
}
/// <summary>
/// IESObject manipulated internally (in the UI)
/// </summary>
[System.Serializable]
public class IESObject : ScriptableObject
{
/// <summary>
/// Metadata of the IES file
/// </summary>
public IESMetaData iesMetaData = new IESMetaData();
}
}

View File

@@ -0,0 +1,540 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using UnityEngine;
namespace UnityEditor.Rendering
{
/// <summary>
/// Class to Parse IES File
/// </summary>
[System.Serializable]
public class IESReader
{
string m_FileFormatVersion;
/// <summary>
/// Version of the IES File
/// </summary>
public string FileFormatVersion
{
get { return m_FileFormatVersion; }
}
float m_TotalLumens;
/// <summary>
/// Total light intensity (in Lumens) stored on the file, usage of it is optional (through the prefab subasset inside the IESObject)
/// </summary>
public float TotalLumens
{
get { return m_TotalLumens; }
}
float m_MaxCandelas;
/// <summary>
/// Maximum of Candela in the IES File
/// </summary>
public float MaxCandelas
{
get { return m_MaxCandelas; }
}
int m_PhotometricType;
/// <summary>
/// Type of Photometric light in the IES file, varying per IES-Type and version
/// </summary>
public int PhotometricType
{
get { return m_PhotometricType; }
}
Dictionary<string, string> m_KeywordDictionary = new Dictionary<string, string>();
int m_VerticalAngleCount;
int m_HorizontalAngleCount;
float[] m_VerticalAngles;
float[] m_HorizontalAngles;
float[] m_CandelaValues;
float m_MinDeltaVerticalAngle;
float m_MinDeltaHorizontalAngle;
float m_FirstHorizontalAngle;
float m_LastHorizontalAngle;
// File format references:
// https://www.ies.org/product/standard-file-format-for-electronic-transfer-of-photometric-data/
// http://lumen.iee.put.poznan.pl/kw/iesna.txt
// https://seblagarde.wordpress.com/2014/11/05/ies-light-format-specification-and-reader/
/// <summary>
/// Main function to read the file
/// </summary>
/// <param name="iesFilePath">The path to the IES File on disk.</param>
/// <returns>Return the error during the import otherwise null if no error</returns>
public string ReadFile(string iesFilePath)
{
using (var iesReader = File.OpenText(iesFilePath))
{
string versionLine = iesReader.ReadLine();
if (versionLine == null)
{
return "Premature end of file (empty file).";
}
switch (versionLine.Trim())
{
case "IESNA91":
m_FileFormatVersion = "LM-63-1991";
break;
case "IESNA:LM-63-1995":
m_FileFormatVersion = "LM-63-1995";
break;
case "IESNA:LM-63-2002":
m_FileFormatVersion = "LM-63-2002";
break;
case "IES:LM-63-2019":
m_FileFormatVersion = "LM-63-2019";
break;
default:
m_FileFormatVersion = "LM-63-1986";
break;
}
var keywordRegex = new Regex(@"\s*\[(?<keyword>\w+)\]\s*(?<data>.*)", RegexOptions.Compiled);
var tiltRegex = new Regex(@"TILT=(?<data>.*)", RegexOptions.Compiled);
string currentKeyword = string.Empty;
for (string keywordLine = (m_FileFormatVersion == "LM-63-1986") ? versionLine : iesReader.ReadLine(); true; keywordLine = iesReader.ReadLine())
{
if (keywordLine == null)
{
return "Premature end of file (missing TILT=NONE).";
}
if (string.IsNullOrWhiteSpace(keywordLine))
{
continue;
}
Match keywordMatch = keywordRegex.Match(keywordLine);
if (keywordMatch.Success)
{
string keyword = keywordMatch.Groups["keyword"].Value;
string data = keywordMatch.Groups["data"].Value.Trim();
if (keyword == currentKeyword || keyword == "MORE")
{
m_KeywordDictionary[currentKeyword] += $" {data}";
}
else
{
// Many separate occurrences of keyword OTHER will need to be handled properly once exposed in the inspector.
currentKeyword = keyword;
m_KeywordDictionary[currentKeyword] = data;
}
continue;
}
Match tiltMatch = tiltRegex.Match(keywordLine);
if (tiltMatch.Success)
{
string data = tiltMatch.Groups["data"].Value.Trim();
if (data == "NONE")
{
break;
}
return $"TILT format not supported: TILT={data}";
}
}
string[] iesDataTokens = Regex.Split(iesReader.ReadToEnd().Trim(), @"[\s,]+");
var iesDataTokenEnumerator = iesDataTokens.GetEnumerator();
string iesDataToken;
if (iesDataTokens.Length == 1 && string.IsNullOrWhiteSpace(iesDataTokens[0]))
{
return "Premature end of file (missing IES data).";
}
if (!iesDataTokenEnumerator.MoveNext())
{
return "Premature end of file (missing lamp count value).";
}
int lampCount;
iesDataToken = iesDataTokenEnumerator.Current.ToString();
if (!int.TryParse(iesDataToken, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out lampCount))
{
return $"Invalid lamp count value: {iesDataToken}";
}
if (lampCount < 1) lampCount = 1;
if (!iesDataTokenEnumerator.MoveNext())
{
return "Premature end of file (missing lumens per lamp value).";
}
float lumensPerLamp;
iesDataToken = iesDataTokenEnumerator.Current.ToString();
if (!float.TryParse(iesDataToken, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out lumensPerLamp))
{
return $"Invalid lumens per lamp value: {iesDataToken}";
}
m_TotalLumens = (lumensPerLamp < 0f) ? -1f : lampCount * lumensPerLamp;
if (!iesDataTokenEnumerator.MoveNext())
{
return "Premature end of file (missing candela multiplier value).";
}
float candelaMultiplier;
iesDataToken = iesDataTokenEnumerator.Current.ToString();
if (!float.TryParse(iesDataToken, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out candelaMultiplier))
{
return $"Invalid candela multiplier value: {iesDataToken}";
}
if (candelaMultiplier < 0f) candelaMultiplier = 0f;
if (!iesDataTokenEnumerator.MoveNext())
{
return "Premature end of file (missing vertical angle count value).";
}
iesDataToken = iesDataTokenEnumerator.Current.ToString();
if (!int.TryParse(iesDataToken, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out m_VerticalAngleCount))
{
return $"Invalid vertical angle count value: {iesDataToken}";
}
if (m_VerticalAngleCount < 1)
{
return $"Invalid number of vertical angles: {m_VerticalAngleCount}";
}
if (!iesDataTokenEnumerator.MoveNext())
{
return "Premature end of file (missing horizontal angle count value).";
}
iesDataToken = iesDataTokenEnumerator.Current.ToString();
if (!int.TryParse(iesDataToken, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out m_HorizontalAngleCount))
{
return $"Invalid horizontal angle count value: {iesDataToken}";
}
if (m_HorizontalAngleCount < 1)
{
return $"Invalid number of horizontal angles: {m_HorizontalAngleCount}";
}
if (!iesDataTokenEnumerator.MoveNext())
{
return "Premature end of file (missing photometric type value).";
}
iesDataToken = iesDataTokenEnumerator.Current.ToString();
if (!int.TryParse(iesDataToken, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out m_PhotometricType))
{
return $"Invalid photometric type value: {iesDataToken}";
}
if (m_PhotometricType < 1 || m_PhotometricType > 3)
{
return $"Invalid photometric type: {m_PhotometricType}";
}
// Skip luminous dimension unit type.
if (!iesDataTokenEnumerator.MoveNext())
{
return "Premature end of file (missing luminous dimension unit type value).";
}
// Skip luminous dimension width.
if (!iesDataTokenEnumerator.MoveNext())
{
return "Premature end of file (missing luminous dimension width value).";
}
// Skip luminous dimension length.
if (!iesDataTokenEnumerator.MoveNext())
{
return "Premature end of file (missing luminous dimension length value).";
}
// Skip luminous dimension height.
if (!iesDataTokenEnumerator.MoveNext())
{
return "Premature end of file (missing luminous dimension height value).";
}
if (!iesDataTokenEnumerator.MoveNext())
{
return "Premature end of file (missing ballast factor value).";
}
float ballastFactor;
iesDataToken = iesDataTokenEnumerator.Current.ToString();
if (!float.TryParse(iesDataToken, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out ballastFactor))
{
return $"Invalid ballast factor value: {iesDataToken}";
}
if (ballastFactor < 0f) ballastFactor = 0f;
// Skip future use.
if (!iesDataTokenEnumerator.MoveNext())
{
return "Premature end of file (missing future use value).";
}
// Skip input watts.
if (!iesDataTokenEnumerator.MoveNext())
{
return "Premature end of file (missing input watts value).";
}
m_VerticalAngles = new float[m_VerticalAngleCount];
float previousVerticalAngle = float.MinValue;
m_MinDeltaVerticalAngle = 180f;
for (int v = 0; v < m_VerticalAngleCount; ++v)
{
if (!iesDataTokenEnumerator.MoveNext())
{
return "Premature end of file (missing vertical angle values).";
}
float angle;
iesDataToken = iesDataTokenEnumerator.Current.ToString();
if (!float.TryParse(iesDataToken, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out angle))
{
return $"Invalid vertical angle value: {iesDataToken}";
}
if (angle <= previousVerticalAngle)
{
return $"Vertical angles are not in ascending order near: {angle}";
}
float deltaVerticalAngle = angle - previousVerticalAngle;
if (deltaVerticalAngle < m_MinDeltaVerticalAngle)
{
m_MinDeltaVerticalAngle = deltaVerticalAngle;
}
m_VerticalAngles[v] = previousVerticalAngle = angle;
}
m_HorizontalAngles = new float[m_HorizontalAngleCount];
float previousHorizontalAngle = float.MinValue;
m_MinDeltaHorizontalAngle = 360f;
for (int h = 0; h < m_HorizontalAngleCount; ++h)
{
if (!iesDataTokenEnumerator.MoveNext())
{
return "Premature end of file (missing horizontal angle values).";
}
float angle;
iesDataToken = iesDataTokenEnumerator.Current.ToString();
if (!float.TryParse(iesDataToken, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out angle))
{
return $"Invalid horizontal angle value: {iesDataToken}";
}
if (angle <= previousHorizontalAngle)
{
return $"Horizontal angles are not in ascending order near: {angle}";
}
float deltaHorizontalAngle = angle - previousHorizontalAngle;
if (deltaHorizontalAngle < m_MinDeltaHorizontalAngle)
{
m_MinDeltaHorizontalAngle = deltaHorizontalAngle;
}
m_HorizontalAngles[h] = previousHorizontalAngle = angle;
}
m_FirstHorizontalAngle = m_HorizontalAngles[0];
m_LastHorizontalAngle = m_HorizontalAngles[m_HorizontalAngleCount - 1];
m_CandelaValues = new float[m_HorizontalAngleCount * m_VerticalAngleCount];
m_MaxCandelas = 0f;
for (int h = 0; h < m_HorizontalAngleCount; ++h)
{
for (int v = 0; v < m_VerticalAngleCount; ++v)
{
if (!iesDataTokenEnumerator.MoveNext())
{
return "Premature end of file (missing candela values).";
}
float value;
iesDataToken = iesDataTokenEnumerator.Current.ToString();
if (!float.TryParse(iesDataToken, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out value))
{
return $"Invalid candela value: {iesDataToken}";
}
value *= candelaMultiplier * ballastFactor;
m_CandelaValues[h * m_VerticalAngleCount + v] = value;
if (value > m_MaxCandelas)
{
m_MaxCandelas = value;
}
}
}
}
return null;
}
internal string GetKeywordValue(string keyword)
{
return m_KeywordDictionary.ContainsKey(keyword) ? m_KeywordDictionary[keyword] : string.Empty;
}
internal int GetMinVerticalSampleCount()
{
if (m_PhotometricType == 2) // type B
{
// Factor in the 90 degree rotation that will be done when building the cylindrical texture.
return 1 + (int)Mathf.Ceil(360 / m_MinDeltaHorizontalAngle); // 360 is 2 * 180 degrees
}
else // type A or C
{
return 1 + (int)Mathf.Ceil(360 / m_MinDeltaVerticalAngle); // 360 is 2 * 180 degrees
}
}
internal int GetMinHorizontalSampleCount()
{
switch (m_PhotometricType)
{
case 3: // type A
return 1 + (int)Mathf.Ceil(720 / m_MinDeltaHorizontalAngle); // 720 is 2 * 360 degrees
case 2: // type B
// Factor in the 90 degree rotation that will be done when building the cylindrical texture.
return 1 + (int)Mathf.Ceil(720 / m_MinDeltaVerticalAngle); // 720 is 2 * 360 degrees
default: // type C
// Factor in the 90 degree rotation that will be done when building the cylindrical texture.
return 1 + (int)Mathf.Ceil(720 / Mathf.Min(m_MinDeltaHorizontalAngle, m_MinDeltaVerticalAngle)); // 720 is 2 * 360 degrees
}
}
internal float ComputeVerticalAnglePosition(float angle)
{
return ComputeAnglePosition(angle, m_VerticalAngles);
}
internal float ComputeTypeAorBHorizontalAnglePosition(float angle) // angle in range [-180..+180] degrees
{
return ComputeAnglePosition(((m_FirstHorizontalAngle == 0f) ? Mathf.Abs(angle) : angle), m_HorizontalAngles);
}
internal float ComputeTypeCHorizontalAnglePosition(float angle) // angle in range [0..360] degrees
{
switch (m_LastHorizontalAngle)
{
case 0f: // the luminaire is assumed to be laterally symmetric in all planes
angle = 0f;
break;
case 90f: // the luminaire is assumed to be symmetric in each quadrant
angle = 90f - Mathf.Abs(Mathf.Abs(angle - 180f) - 90f);
break;
case 180f: // the luminaire is assumed to be symmetric about the 0 to 180 degree plane
angle = 180f - Mathf.Abs(angle - 180f);
break;
default: // the luminaire is assumed to exhibit no lateral symmetry
break;
}
return ComputeAnglePosition(angle, m_HorizontalAngles);
}
internal float ComputeAnglePosition(float value, float[] angles)
{
int start = 0;
int end = angles.Length - 1;
if (value < angles[start])
{
return start;
}
if (value > angles[end])
{
return end;
}
while (start < end)
{
int index = (start + end + 1) / 2;
float angle = angles[index];
if (value >= angle)
{
start = index;
}
else
{
end = index - 1;
}
}
float leftValue = angles[start];
float fraction = 0f;
if (start + 1 < angles.Length)
{
float rightValue = angles[start + 1];
float deltaValue = rightValue - leftValue;
if (deltaValue > 0.0001f)
{
fraction = (value - leftValue) / deltaValue;
}
}
return start + fraction;
}
internal float InterpolateBilinear(float x, float y)
{
int ix = (int)Mathf.Floor(x);
int iy = (int)Mathf.Floor(y);
float fractionX = x - ix;
float fractionY = y - iy;
float p00 = InterpolatePoint(ix + 0, iy + 0);
float p10 = InterpolatePoint(ix + 1, iy + 0);
float p01 = InterpolatePoint(ix + 0, iy + 1);
float p11 = InterpolatePoint(ix + 1, iy + 1);
float p0 = Mathf.Lerp(p00, p01, fractionY);
float p1 = Mathf.Lerp(p10, p11, fractionY);
return Mathf.Lerp(p0, p1, fractionX);
}
internal float InterpolatePoint(int x, int y)
{
x %= m_HorizontalAngles.Length;
y %= m_VerticalAngles.Length;
return m_CandelaValues[y + x * m_VerticalAngles.Length];
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1,535 @@
using System;
using UnityEngine;
using UnityEditor.ShortcutManagement;
using UnityEngine.UIElements;
namespace UnityEditor.Rendering.LookDev
{
class CameraController : Manipulator
{
[Flags]
enum Direction
{
None = 0,
Up = 1 << 0,
Down = 1 << 1,
Left = 1 << 2,
Right = 1 << 3,
Forward = 1 << 4,
Backward = 1 << 5,
All = Up | Down | Left | Right | Forward | Backward
}
Direction m_DirectionKeyPressed = Direction.None;
float m_StartZoom = 0.0f;
float m_ZoomSpeed = 0.0f;
float m_TotalMotion = 0.0f;
Vector3 m_MotionDirection = new Vector3();
float m_FlySpeedNormalized = .5f;
float m_FlySpeed = 1f;
float m_FlySpeedAccelerated = 0f;
const float m_FlySpeedMin = .01f;
const float m_FlySpeedMax = 2f;
//[TODO: check if necessary to add hability to deactivate acceleration]
const float k_FlyAcceleration = 1.1f;
bool m_ShiftBoostedFly = false;
bool m_InFlyMotion;
bool m_IsDragging;
static TimeHelper s_Timer = new TimeHelper();
ViewTool m_BehaviorState;
ViewTool behaviorState
{
get { return m_BehaviorState; }
set
{
if (value != m_BehaviorState && m_BehaviorState == ViewTool.FPS)
{
isDragging = false;
inFlyMotion = false;
m_DirectionKeyPressed = Direction.None;
}
m_BehaviorState = value;
}
}
protected CameraState m_CameraState;
DisplayWindow m_Window;
protected Action m_Focused;
Rect screen => target.contentRect;
bool inFlyMotion
{
get => m_InFlyMotion;
set
{
if (value ^ m_InFlyMotion)
{
if (value)
{
s_Timer.Begin();
EditorApplication.update += UpdateFPSMotion;
}
else
{
m_FlySpeedAccelerated = 0f;
m_MotionDirection = Vector3.zero;
m_ShiftBoostedFly = false;
EditorApplication.update -= UpdateFPSMotion;
}
m_InFlyMotion = value;
}
}
}
float flySpeedNormalized
{
get => m_FlySpeedNormalized;
set
{
m_FlySpeedNormalized = Mathf.Clamp01(value);
float speed = Mathf.Lerp(m_FlySpeedMin, m_FlySpeedMax, m_FlySpeedNormalized);
// Round to nearest decimal: 2 decimal points when between [0.01, 0.1]; 1 decimal point when between [0.1, 10]; integral between [10, 99]
speed = (float)(System.Math.Round((double)speed, speed < 0.1f ? 2 : speed < 10f ? 1 : 0));
m_FlySpeed = Mathf.Clamp(speed, m_FlySpeedMin, m_FlySpeedMax);
}
}
float flySpeed
{
get => m_FlySpeed;
set => flySpeedNormalized = Mathf.InverseLerp(m_FlySpeedMin, m_FlySpeedMax, value);
}
virtual protected bool isDragging
{
get => m_IsDragging;
set
{
//As in scene view, stop dragging as first button is release in case of multiple button down
if (value ^ m_IsDragging)
{
if (value)
{
target.RegisterCallback<MouseMoveEvent>(OnMouseDrag);
target.CaptureMouse();
EditorGUIUtility.SetWantsMouseJumping(1); //through screen edges
}
else
{
EditorGUIUtility.SetWantsMouseJumping(0);
target.ReleaseMouse();
target.UnregisterCallback<MouseMoveEvent>(OnMouseDrag);
}
m_IsDragging = value;
}
}
}
public CameraController(DisplayWindow window, Action focused)
{
m_Window = window;
m_Focused = focused;
}
public void UpdateCameraState(Context context, ViewIndex index)
{
m_CameraState = context.GetViewContent(index).camera;
}
private void ResetCameraControl()
{
isDragging = false;
inFlyMotion = false;
behaviorState = ViewTool.None;
}
protected virtual void OnScrollWheel(WheelEvent evt)
{
// See UnityEditor.SceneViewMotion.HandleScrollWheel
switch (behaviorState)
{
case ViewTool.FPS: OnChangeFPSCameraSpeed(evt); break;
default: OnZoom(evt); break;
}
}
void OnMouseDrag(MouseMoveEvent evt)
{
switch (behaviorState)
{
case ViewTool.Orbit: OnMouseDragOrbit(evt); break;
case ViewTool.FPS: OnMouseDragFPS(evt); break;
case ViewTool.Pan: OnMouseDragPan(evt); break;
case ViewTool.Zoom: OnMouseDragZoom(evt); break;
default: break;
}
}
void OnKeyDown(KeyDownEvent evt)
{
OnKeyUpOrDownFPS(evt);
OnKeyDownReset(evt);
}
void OnChangeFPSCameraSpeed(WheelEvent evt)
{
float scrollWheelDelta = evt.delta.y;
flySpeedNormalized -= scrollWheelDelta * .01f;
string cameraSpeedDisplayValue = flySpeed.ToString(flySpeed < 0.1f ? "F2" : flySpeed < 10f ? "F1" : "F0");
if (flySpeed < 0.1f)
cameraSpeedDisplayValue = cameraSpeedDisplayValue.TrimStart(new Char[] { '0' });
GUIContent cameraSpeedContent = EditorGUIUtility.TrTempContent(
$"{cameraSpeedDisplayValue}x");
m_Window.ShowNotification(cameraSpeedContent, .5f);
evt.StopPropagation();
}
void OnZoom(WheelEvent evt)
{
const float deltaCutoff = .3f;
const float minZoom = .003f;
float scrollWheelDelta = evt.delta.y;
float relativeDelta = m_CameraState.viewSize * scrollWheelDelta * .015f;
if (relativeDelta > 0 && relativeDelta < deltaCutoff)
relativeDelta = deltaCutoff;
else if (relativeDelta <= 0 && relativeDelta > -deltaCutoff)
relativeDelta = -deltaCutoff;
m_CameraState.viewSize += relativeDelta;
if (m_CameraState.viewSize < minZoom)
m_CameraState.viewSize = minZoom;
evt.StopPropagation();
}
void OnMouseDragOrbit(MouseMoveEvent evt)
{
Quaternion rotation = m_CameraState.rotation;
rotation = Quaternion.AngleAxis(evt.mouseDelta.y * .003f * Mathf.Rad2Deg, rotation * Vector3.right) * rotation;
rotation = Quaternion.AngleAxis(evt.mouseDelta.x * .003f * Mathf.Rad2Deg, Vector3.up) * rotation;
m_CameraState.rotation = rotation;
evt.StopPropagation();
}
void OnMouseDragFPS(MouseMoveEvent evt)
{
Vector3 camPos = m_CameraState.pivot - m_CameraState.rotation * Vector3.forward * m_CameraState.distanceFromPivot;
Quaternion rotation = m_CameraState.rotation;
rotation = Quaternion.AngleAxis(evt.mouseDelta.y * .003f * Mathf.Rad2Deg, rotation * Vector3.right) * rotation;
rotation = Quaternion.AngleAxis(evt.mouseDelta.x * .003f * Mathf.Rad2Deg, Vector3.up) * rotation;
m_CameraState.rotation = rotation;
m_CameraState.pivot = camPos + rotation * Vector3.forward * m_CameraState.distanceFromPivot;
evt.StopPropagation();
}
void OnMouseDragPan(MouseMoveEvent evt)
{
//[TODO: fix WorldToScreenPoint and ScreenToWorldPoint
var screenPos = m_CameraState.QuickProjectPivotInScreen(screen);
screenPos += new Vector3(evt.mouseDelta.x, -evt.mouseDelta.y, 0);
//Vector3 newWorldPos = m_CameraState.ScreenToWorldPoint(screen, screenPos);
Vector3 newWorldPos = m_CameraState.QuickReprojectionWithFixedFOVOnPivotPlane(screen, screenPos);
Vector3 worldDelta = newWorldPos - m_CameraState.pivot;
worldDelta *= EditorGUIUtility.pixelsPerPoint;
if (evt.shiftKey)
worldDelta *= 4;
m_CameraState.pivot += worldDelta;
evt.StopPropagation();
}
void OnMouseDragZoom(MouseMoveEvent evt)
{
float zoomDelta = HandleUtility.niceMouseDeltaZoom * (evt.shiftKey ? 9 : 3);
m_TotalMotion += zoomDelta;
if (m_TotalMotion < 0)
m_CameraState.viewSize = m_StartZoom * (1 + m_TotalMotion * .001f);
else
m_CameraState.viewSize = m_CameraState.viewSize + zoomDelta * m_ZoomSpeed * .003f;
evt.StopPropagation();
}
void OnKeyDownReset(KeyDownEvent evt)
{
if (evt.keyCode == KeyCode.Escape)
ResetCameraControl();
evt.StopPropagation();
}
void OnKeyUpOrDownFPS<T>(KeyboardEventBase<T> evt)
where T : KeyboardEventBase<T>, new()
{
if (behaviorState != ViewTool.FPS)
return;
//Note: Keydown is called in loop but between first occurence of the
// loop and laters, there is a small pause. To deal with this, we
// need to register the UpdateMovement function to the Editor update
KeyCombination combination;
if (GetKeyCombinationByID("3D Viewport/Fly Mode Forward", out combination) && combination.Match(evt))
RegisterMotionChange(Direction.Forward, evt);
if (GetKeyCombinationByID("3D Viewport/Fly Mode Backward", out combination) && combination.Match(evt))
RegisterMotionChange(Direction.Backward, evt);
if (GetKeyCombinationByID("3D Viewport/Fly Mode Left", out combination) && combination.Match(evt))
RegisterMotionChange(Direction.Left, evt);
if (GetKeyCombinationByID("3D Viewport/Fly Mode Right", out combination) && combination.Match(evt))
RegisterMotionChange(Direction.Right, evt);
if (GetKeyCombinationByID("3D Viewport/Fly Mode Up", out combination) && combination.Match(evt))
RegisterMotionChange(Direction.Up, evt);
if (GetKeyCombinationByID("3D Viewport/Fly Mode Down", out combination) && combination.Match(evt))
RegisterMotionChange(Direction.Down, evt);
}
void RegisterMotionChange<T>(Direction direction, KeyboardEventBase<T> evt)
where T : KeyboardEventBase<T>, new()
{
m_ShiftBoostedFly = evt.shiftKey;
Direction formerDirection = m_DirectionKeyPressed;
bool keyUp = evt is KeyUpEvent;
bool keyDown = evt is KeyDownEvent;
if (keyDown)
m_DirectionKeyPressed |= direction;
else if (keyUp)
m_DirectionKeyPressed &= (Direction.All & ~direction);
if (formerDirection != m_DirectionKeyPressed)
{
m_MotionDirection = new Vector3(
((m_DirectionKeyPressed & Direction.Right) > 0 ? 1 : 0) - ((m_DirectionKeyPressed & Direction.Left) > 0 ? 1 : 0),
((m_DirectionKeyPressed & Direction.Up) > 0 ? 1 : 0) - ((m_DirectionKeyPressed & Direction.Down) > 0 ? 1 : 0),
((m_DirectionKeyPressed & Direction.Forward) > 0 ? 1 : 0) - ((m_DirectionKeyPressed & Direction.Backward) > 0 ? 1 : 0));
inFlyMotion = m_DirectionKeyPressed != Direction.None;
}
evt.StopPropagation();
}
Vector3 GetMotionDirection()
{
var deltaTime = s_Timer.Update();
Vector3 result;
float speed = (m_ShiftBoostedFly ? 5 * flySpeed : flySpeed);
if (m_FlySpeedAccelerated == 0)
m_FlySpeedAccelerated = 9;
else
m_FlySpeedAccelerated *= Mathf.Pow(k_FlyAcceleration, deltaTime);
result = m_MotionDirection.normalized * m_FlySpeedAccelerated * speed * deltaTime;
return result;
}
void UpdateFPSMotion()
{
m_CameraState.pivot += m_CameraState.rotation * GetMotionDirection();
m_Window.Repaint(); //this prevent hich on key down as in CameraFlyModeContext.cs
}
bool GetKeyCombinationByID(string ID, out KeyCombination combination)
{
var sequence = ShortcutManager.instance.GetShortcutBinding(ID).keyCombinationSequence.GetEnumerator();
if (sequence.MoveNext()) //have a first entry
{
combination = new KeyCombination(sequence.Current);
return true;
}
else
{
combination = default;
return false;
}
}
ViewTool GetBehaviorTool<T>(MouseEventBase<T> evt, bool onMac) where T : MouseEventBase<T>, new()
{
if (evt.button == 2)
return ViewTool.Pan;
else if (evt.button == 0 && evt.ctrlKey && onMac || evt.button == 1 && evt.altKey)
return ViewTool.Zoom;
else if (evt.button == 0)
return ViewTool.Orbit;
else if (evt.button == 1 && !evt.altKey)
return ViewTool.FPS;
return ViewTool.None;
}
void OnMouseUp(MouseUpEvent evt)
{
bool onMac = Application.platform == RuntimePlatform.OSXEditor;
var state = GetBehaviorTool(evt, onMac);
if (state == behaviorState)
ResetCameraControl();
evt.StopPropagation();
}
void OnMouseDown(MouseDownEvent evt)
{
bool onMac = Application.platform == RuntimePlatform.OSXEditor;
behaviorState = GetBehaviorTool(evt, onMac);
if (behaviorState == ViewTool.Zoom)
{
m_StartZoom = m_CameraState.viewSize;
m_ZoomSpeed = Mathf.Max(Mathf.Abs(m_StartZoom), .3f);
m_TotalMotion = 0;
}
// see also SceneView.HandleClickAndDragToFocus()
if (evt.button == 1 && onMac)
m_Window.Focus();
target.Focus(); //required for keyboard event
isDragging = true;
evt.StopPropagation();
m_Focused?.Invoke();
}
protected override void RegisterCallbacksOnTarget()
{
target.focusable = true; //prerequisite for being focusable and recerive keydown events
target.RegisterCallback<MouseUpEvent>(OnMouseUp);
target.RegisterCallback<MouseDownEvent>(OnMouseDown);
target.RegisterCallback<WheelEvent>(OnScrollWheel);
target.RegisterCallback<KeyDownEvent>(OnKeyDown);
target.RegisterCallback<KeyUpEvent>(OnKeyUpOrDownFPS);
}
protected override void UnregisterCallbacksFromTarget()
{
target.UnregisterCallback<MouseUpEvent>(OnMouseUp);
target.UnregisterCallback<MouseDownEvent>(OnMouseDown);
target.UnregisterCallback<WheelEvent>(OnScrollWheel);
target.UnregisterCallback<KeyDownEvent>(OnKeyDown);
target.UnregisterCallback<KeyUpEvent>(OnKeyUpOrDownFPS);
}
struct KeyCombination
{
KeyCode key;
EventModifiers modifier;
public bool shiftOnLastMatch;
public KeyCombination(UnityEditor.ShortcutManagement.KeyCombination shortcutCombination)
{
key = shortcutCombination.keyCode;
modifier = EventModifiers.None;
if ((shortcutCombination.modifiers & ShortcutModifiers.Shift) != 0)
modifier |= EventModifiers.Shift;
if ((shortcutCombination.modifiers & ShortcutModifiers.Alt) != 0)
modifier |= EventModifiers.Alt;
if ((shortcutCombination.modifiers & ShortcutModifiers.Action) != 0)
{
if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.OSXPlayer)
modifier |= EventModifiers.Command;
else
modifier |= EventModifiers.Control;
}
shiftOnLastMatch = false;
}
//atLeastModifier allow case were A is required but event provide shift+A
public bool Match(IKeyboardEvent evt, bool atLeastForModifier = true)
{
shiftOnLastMatch = evt.shiftKey;
if (atLeastForModifier)
return key == evt.keyCode && modifier == (evt.modifiers & modifier);
else
return key == evt.keyCode && modifier == evt.modifiers;
}
}
struct TimeHelper
{
long lastTime;
public void Begin() => lastTime = System.DateTime.Now.Ticks;
public float Update()
{
float deltaTime = (System.DateTime.Now.Ticks - lastTime) / 10000000.0f;
lastTime = System.DateTime.Now.Ticks;
return deltaTime;
}
}
}
class SwitchableCameraController : CameraController
{
CameraState m_FirstView;
CameraState m_SecondView;
ViewIndex m_CurrentViewIndex;
bool switchedDrag = false;
bool switchedWheel = false;
public SwitchableCameraController(DisplayWindow window, Action<ViewIndex> focused)
: base(window, null)
{
m_CurrentViewIndex = ViewIndex.First;
m_Focused = () => focused?.Invoke(m_CurrentViewIndex);
}
public void UpdateCameraState(Context context)
{
m_FirstView = context.GetViewContent(ViewIndex.First).camera;
m_SecondView = context.GetViewContent(ViewIndex.Second).camera;
m_CameraState = m_CurrentViewIndex == ViewIndex.First ? m_FirstView : m_SecondView;
}
void SwitchTo(ViewIndex index)
{
CameraState stateToSwitch;
switch (index)
{
case ViewIndex.First:
stateToSwitch = m_FirstView;
break;
case ViewIndex.Second:
stateToSwitch = m_SecondView;
break;
default:
throw new ArgumentException("Unknown ViewIndex");
}
if (stateToSwitch != m_CameraState)
m_CameraState = stateToSwitch;
m_CurrentViewIndex = index;
}
public void SwitchUntilNextEndOfDrag()
{
switchedDrag = true;
SwitchTo(ViewIndex.Second);
}
override protected bool isDragging
{
get => base.isDragging;
set
{
bool switchBack = false;
if (switchedDrag && base.isDragging && !value)
switchBack = true;
base.isDragging = value;
if (switchBack)
SwitchTo(ViewIndex.First);
}
}
public void SwitchUntilNextWheelEvent()
{
switchedWheel = true;
SwitchTo(ViewIndex.Second);
}
protected override void OnScrollWheel(WheelEvent evt)
{
base.OnScrollWheel(evt);
if (switchedWheel)
SwitchTo(ViewIndex.First);
}
}
}

View File

@@ -0,0 +1,119 @@
using UnityEditor.AnimatedValues;
using UnityEngine;
namespace UnityEditor.Rendering.LookDev
{
/// <summary>
/// Interface to comunicate with simple <see cref="Renderer"/>
/// </summary>
public interface ICameraUpdater
{
/// <summary>Method called To update the LookDev camera position</summary>
/// <param name="camera">The camera</param>
void UpdateCamera(Camera camera);
}
/// <summary>
/// Class containing data regarding position, rotation and viewport size of a camera
/// </summary>
[System.Serializable]
public class CameraState : ICameraUpdater
{
private static readonly Quaternion k_DefaultRotation = Quaternion.LookRotation(new Vector3(0.0f, 0.0f, 1.0f));
private const float k_DefaultViewSize = 10f;
private static readonly Vector3 k_DefaultPivot = Vector3.zero;
private const float k_DefaultFoV = 90f;
private const float k_NearFactor = 0.000005f;
private const float k_MaxFar = 1000;
/// <summary>The position of the camera pivot</summary>
[field: SerializeField]
public Vector3 pivot { get; set; } = k_DefaultPivot;
/// <summary>The rotation of the camera arround the pivot</summary>
[field: SerializeField]
public Quaternion rotation { get; set; } = k_DefaultRotation;
/// <summary>The size of the view</summary>
[field: SerializeField]
public float viewSize { get; set; } = k_DefaultViewSize;
/// <summary>The distance from pivot</summary>
public float distanceFromPivot
// distance coeficient from vertical FOV should be
// 1f / Mathf.Tan(kDefaultFoV * 0.5f * Mathf.Deg2Rad)
// but with fixed FoV of 90, this coef is always equal to 1f
=> viewSize;
/// <summary>The position of the camera</summary>
public Vector3 position
=> pivot + rotation * new Vector3(0, 0, -distanceFromPivot);
/// <summary>The field of view of the camera</summary>
public float fieldOfView => k_DefaultFoV;
/// <summary>The far clip distance from camera</summary>
public float farClip => Mathf.Max(k_MaxFar, 2 * k_MaxFar * viewSize);
/// <summary>The near clip distance from camera</summary>
public float nearClip => farClip * k_NearFactor;
/// <summary>The Forward vector in world space</summary>
public Vector3 forward => rotation * Vector3.forward;
/// <summary>The Up vector in world space</summary>
public Vector3 up => rotation * Vector3.up;
/// <summary>The Right vector in world space</summary>
public Vector3 right => rotation * Vector3.right;
internal Vector3 QuickReprojectionWithFixedFOVOnPivotPlane(Rect screen, Vector2 screenPoint)
{
if (screen.height == 0)
return Vector3.zero;
float aspect = screen.width / screen.height;
//Note: verticalDistance is same than distance from pivot with fixed FoV 90°
float verticalDistance = distanceFromPivot;
Vector2 normalizedScreenPoint = new Vector2(
screenPoint.x * 2f / screen.width - 1f,
screenPoint.y * 2f / screen.height - 1f);
return pivot
- up * verticalDistance * normalizedScreenPoint.y
- right * verticalDistance * aspect * normalizedScreenPoint.x;
}
//Pivot is always on center axis by construction
internal Vector3 QuickProjectPivotInScreen(Rect screen)
=> new Vector3(screen.width * .5f, screen.height * .5f, distanceFromPivot);
/// <summary>
/// Update a Camera component and its transform with this state values
/// </summary>
/// <param name="camera">The camera to update</param>
public void UpdateCamera(Camera camera)
{
camera.transform.rotation = rotation;
camera.transform.position = position;
camera.nearClipPlane = nearClip;
camera.farClipPlane = farClip;
camera.fieldOfView = fieldOfView;
}
/// <summary>
/// Reset the State to its default values
/// </summary>
public void Reset()
{
pivot = k_DefaultPivot;
rotation = k_DefaultRotation;
viewSize = k_DefaultViewSize;
}
internal void SynchronizeFrom(CameraState other)
{
pivot = other.pivot;
rotation = other.rotation;
viewSize = other.viewSize;
}
}
}

View File

@@ -0,0 +1,244 @@
using System;
using UnityEngine.UIElements;
using UnityEngine;
namespace UnityEditor.Rendering.LookDev
{
//TODO: clamps to always have both node on screen
class ComparisonGizmoController : Manipulator
{
const float k_DragPadding = 0.05f;
const float k_ReferenceScale = 1080f;
ComparisonGizmoState m_State;
SwitchableCameraController m_Switcher;
enum Selected
{
None,
NodeFirstView,
NodeSecondView,
PlaneSeparator,
Fader
}
Selected m_Selected;
Vector2 m_SavedRelativePositionOnMouseDown;
bool m_IsDragging;
bool isDragging
{
get => m_IsDragging;
set
{
//As in scene view, stop dragging as first button is release in case of multiple button down
if (value ^ m_IsDragging)
{
if (value)
{
target.RegisterCallback<MouseMoveEvent>(OnMouseDrag);
target.CaptureMouse();
}
else
{
target.ReleaseMouse();
target.UnregisterCallback<MouseMoveEvent>(OnMouseDrag);
}
m_IsDragging = value;
}
}
}
public ComparisonGizmoController(SwitchableCameraController switcher)
{
m_Switcher = switcher;
}
public void UpdateGizmoState(ComparisonGizmoState state)
{
m_State = state;
}
protected override void RegisterCallbacksOnTarget()
{
target.RegisterCallback<MouseDownEvent>(OnMouseDown);
target.RegisterCallback<MouseUpEvent>(OnMouseUp);
target.RegisterCallback<WheelEvent>(OnScrollWheel);
}
protected override void UnregisterCallbacksFromTarget()
{
target.UnregisterCallback<MouseDownEvent>(OnMouseDown);
target.UnregisterCallback<MouseUpEvent>(OnMouseUp);
target.UnregisterCallback<WheelEvent>(OnScrollWheel);
}
void OnScrollWheel(WheelEvent evt)
{
if (LookDev.currentContext.layout.viewLayout != Layout.CustomSplit)
return;
if (GetViewFromComposition(evt.localMousePosition) == ViewIndex.Second)
m_Switcher.SwitchUntilNextWheelEvent();
//let event be propagated to views
}
void OnMouseDown(MouseDownEvent evt)
{
if (LookDev.currentContext.layout.viewLayout != Layout.CustomSplit)
return;
Rect displayRect = target.contentRect;
SelectGizmoZone(GetNormalizedCoordinates(evt.localMousePosition, displayRect));
if (m_Selected != Selected.None)
{
m_SavedRelativePositionOnMouseDown = GetNormalizedCoordinates(evt.localMousePosition, displayRect) - m_State.center;
isDragging = true;
//We do not want to move camera and gizmo at the same time.
evt.StopImmediatePropagation();
}
else
{
//else let event be propagated to views
if (GetViewFromComposition(evt.localMousePosition) == ViewIndex.Second)
m_Switcher.SwitchUntilNextEndOfDrag();
}
}
void OnMouseUp(MouseUpEvent evt)
{
if (LookDev.currentContext.layout.viewLayout != Layout.CustomSplit
|| m_Selected == Selected.None)
return;
// deadzone in fader gizmo
if (m_Selected == Selected.Fader && Mathf.Abs(m_State.blendFactor) < ComparisonGizmoState.circleRadiusSelected / (m_State.length - ComparisonGizmoState.circleRadius))
m_State.blendFactor = 0f;
m_Selected = Selected.None;
isDragging = false;
//We do not want to move camera and gizmo at the same time.
evt.StopImmediatePropagation();
LookDev.SaveConfig();
}
void OnMouseDrag(MouseMoveEvent evt)
{
if (m_Selected == Selected.None)
return;
switch (m_Selected)
{
case Selected.PlaneSeparator: OnDragPlaneSeparator(evt); break;
case Selected.NodeFirstView:
case Selected.NodeSecondView: OnDragPlaneNodeExtremity(evt); break;
case Selected.Fader: OnDragFader(evt); break;
default: throw new ArgumentException("Unknown kind of Selected");
}
}
void OnDragPlaneSeparator(MouseMoveEvent evt)
{
//TODO: handle case when resizing window (clamping)
Vector2 newPosition = GetNormalizedCoordinates(evt.localMousePosition, target.contentRect) - m_SavedRelativePositionOnMouseDown;
// We clamp the center of the gizmo to the border of the screen in order to avoid being able to put it out of the screen.
// The safe band is here to ensure that you always see at least part of the gizmo in order to be able to grab it again.
//Vector2 extends = GetNormalizedCoordinates(new Vector2(displayRect.width, displayRect.height), displayRect);
//newPosition.x = Mathf.Clamp(newPosition.x, -extends.x + k_DragPadding, extends.x - k_DragPadding);
//newPosition.y = Mathf.Clamp(newPosition.y, -extends.y + k_DragPadding, extends.y - k_DragPadding);
m_State.Update(newPosition, m_State.length, m_State.angle);
//We do not want to move camera and gizmo at the same time.
evt.StopImmediatePropagation();
}
void OnDragPlaneNodeExtremity(MouseMoveEvent evt)
{
Vector2 normalizedCoord = GetNormalizedCoordinates(evt.localMousePosition, target.contentRect);
Vector2 basePoint, newPoint;
float angleSnapping = Mathf.Deg2Rad * 45.0f * 0.5f;
newPoint = normalizedCoord;
basePoint = m_Selected == Selected.NodeFirstView ? m_State.point2 : m_State.point1;
// Snap to a multiple of "angleSnapping"
if ((evt.modifiers & EventModifiers.Shift) != 0)
{
Vector3 verticalPlane = new Vector3(-1.0f, 0.0f, basePoint.x);
float side = Vector3.Dot(new Vector3(normalizedCoord.x, normalizedCoord.y, 1.0f), verticalPlane);
float angle = Mathf.Deg2Rad * Vector2.Angle(new Vector2(0.0f, 1.0f), normalizedCoord - basePoint);
if (side > 0.0f)
angle = 2.0f * Mathf.PI - angle;
angle = (int)(angle / angleSnapping) * angleSnapping;
Vector2 dir = normalizedCoord - basePoint;
float length = dir.magnitude;
newPoint = basePoint + new Vector2(Mathf.Sin(angle), Mathf.Cos(angle)) * length;
}
if (m_Selected == Selected.NodeFirstView)
m_State.Update(newPoint, basePoint);
else
m_State.Update(basePoint, newPoint);
//We do not want to move camera and gizmo at the same time.
evt.StopImmediatePropagation();
}
void OnDragFader(MouseMoveEvent evt)
{
Vector2 mousePosition = GetNormalizedCoordinates(evt.localMousePosition, target.contentRect);
float distanceToOrthoPlane = -Vector3.Dot(new Vector3(mousePosition.x, mousePosition.y, 1.0f), m_State.planeOrtho) / m_State.blendFactorMaxGizmoDistance;
m_State.blendFactor = Mathf.Clamp(distanceToOrthoPlane, -1.0f, 1.0f);
//We do not want to move camera and gizmo at the same time.
evt.StopImmediatePropagation();
}
void SelectGizmoZone(Vector2 normalizedMousePosition)
{
//TODO: Optimize
Vector3 normalizedMousePositionZ1 = new Vector3(normalizedMousePosition.x, normalizedMousePosition.y, 1.0f);
float distanceToPlane = Vector3.Dot(normalizedMousePositionZ1, m_State.plane);
float absDistanceToPlane = Mathf.Abs(distanceToPlane);
float distanceFromCenter = Vector2.Distance(normalizedMousePosition, m_State.center);
float distanceToOrtho = Vector3.Dot(normalizedMousePositionZ1, m_State.planeOrtho);
float side = (distanceToOrtho > 0.0f) ? 1.0f : -1.0f;
Vector2 orthoPlaneNormal = new Vector2(m_State.planeOrtho.x, m_State.planeOrtho.y);
Selected selected = Selected.None;
if (absDistanceToPlane < ComparisonGizmoState.circleRadiusSelected && (distanceFromCenter < (m_State.length + ComparisonGizmoState.circleRadiusSelected)))
{
if (absDistanceToPlane < ComparisonGizmoState.thicknessSelected)
selected = Selected.PlaneSeparator;
Vector2 circleCenter = m_State.center + side * orthoPlaneNormal * m_State.length;
float d = Vector2.Distance(normalizedMousePosition, circleCenter);
if (d <= ComparisonGizmoState.circleRadiusSelected)
selected = side > 0.0f ? Selected.NodeFirstView : Selected.NodeSecondView;
float maxBlendCircleDistanceToCenter = m_State.blendFactorMaxGizmoDistance;
float blendCircleDistanceToCenter = m_State.blendFactor * maxBlendCircleDistanceToCenter;
Vector2 blendCircleCenter = m_State.center - orthoPlaneNormal * blendCircleDistanceToCenter;
float blendCircleSelectionRadius = Mathf.Lerp(ComparisonGizmoState.blendFactorCircleRadius, ComparisonGizmoState.blendFactorCircleRadiusSelected, Mathf.Clamp((maxBlendCircleDistanceToCenter - Mathf.Abs(blendCircleDistanceToCenter)) / (ComparisonGizmoState.blendFactorCircleRadiusSelected - ComparisonGizmoState.blendFactorCircleRadius), 0.0f, 1.0f));
if ((normalizedMousePosition - blendCircleCenter).magnitude < blendCircleSelectionRadius)
selected = Selected.Fader;
}
m_Selected = selected;
}
//normalize in [-1,1]^2 for a 1080^2. Can be above 1 for higher than 1080.
internal static Vector2 GetNormalizedCoordinates(Vector2 localMousePosition, Rect rect)
=> new Vector2(
(2f * localMousePosition.x - rect.width) / k_ReferenceScale,
(-2f * localMousePosition.y + rect.height) / k_ReferenceScale);
ViewIndex GetViewFromComposition(Vector2 localCoordinate)
{
Vector2 normalizedLocalCoordinate = GetNormalizedCoordinates(localCoordinate, target.contentRect);
return Vector3.Dot(new Vector3(normalizedLocalCoordinate.x, normalizedLocalCoordinate.y, 1), m_State.plane) >= 0
? ViewIndex.First
: ViewIndex.Second;
}
}
}

View File

@@ -0,0 +1,118 @@
using System;
using UnityEditor.AnimatedValues;
using UnityEngine;
namespace UnityEditor.Rendering.LookDev
{
/// <summary>state of the comparison gizmo of the LookDev</summary>
[Serializable]
public class ComparisonGizmoState
{
internal const float thickness = 0.0028f;
internal const float thicknessSelected = 0.015f;
internal const float circleRadius = 0.014f;
internal const float circleRadiusSelected = 0.03f;
internal const float blendFactorCircleRadius = 0.01f;
internal const float blendFactorCircleRadiusSelected = 0.03f;
/// <summary>Position of the first extremity</summary>
public Vector2 point1 { get; private set; }
/// <summary>Position of the second extremity</summary>
public Vector2 point2 { get; private set; }
/// <summary>Position of the center</summary>
[field: SerializeField]
public Vector2 center { get; private set; } = Vector2.zero;
/// <summary>Angle from vertical in radian</summary>
[field: SerializeField]
public float angle { get; private set; }
/// <summary>Length between extremity</summary>
[field: SerializeField]
public float length { get; private set; } = 0.2f;
internal Vector4 plane { get; private set; }
internal Vector4 planeOrtho { get; private set; }
/// <summary>
/// The position of the blending slider.
/// From value -1 on first extremity to value 1 on second extremity.
/// </summary>
[field: SerializeField]
public float blendFactor { get; set; }
internal float blendFactorMaxGizmoDistance
=> length - circleRadius - blendFactorCircleRadius;
internal float blendFactorMinGizmoDistance
=> length - circleRadius - blendFactorCircleRadiusSelected;
internal void Init()
=> Update(center, length, angle);
//TODO: optimize
private Vector4 Get2DPlane(Vector2 firstPoint, float angle)
{
Vector4 result = new Vector4();
angle = angle % (2.0f * (float)Math.PI);
Vector2 secondPoint = new Vector2(firstPoint.x + Mathf.Sin(angle), firstPoint.y + Mathf.Cos(angle));
Vector2 diff = secondPoint - firstPoint;
if (Mathf.Abs(diff.x) < 1e-5)
{
result.Set(-1.0f, 0.0f, firstPoint.x, 0.0f);
float sign = Mathf.Cos(angle) > 0.0f ? 1.0f : -1.0f;
result *= sign;
}
else
{
float slope = diff.y / diff.x;
result.Set(-slope, 1.0f, -(firstPoint.y - slope * firstPoint.x), 0.0f);
}
if (angle > Mathf.PI)
result = -result;
float length = Mathf.Sqrt(result.x * result.x + result.y * result.y);
result = result / length;
return result;
}
/// <summary>
/// Update all fields while moving one extremity
/// </summary>
/// <param name="point1">The new first extremity position</param>
/// <param name="point2">The new second extremity position</param>
public void Update(Vector2 point1, Vector2 point2)
{
this.point1 = point1;
this.point2 = point2;
center = (point1 + point2) * 0.5f;
length = (point2 - point1).magnitude * 0.5f;
Vector3 verticalPlane = Get2DPlane(center, 0.0f);
float side = Vector3.Dot(new Vector3(point1.x, point1.y, 1.0f), verticalPlane);
angle = (Mathf.Deg2Rad * Vector2.Angle(new Vector2(0.0f, 1.0f), (point1 - point2).normalized));
if (side > 0.0f)
angle = 2.0f * Mathf.PI - angle;
plane = Get2DPlane(center, angle);
planeOrtho = Get2DPlane(center, angle + 0.5f * (float)Mathf.PI);
}
/// <summary>
/// Update all fields while moving the bar
/// </summary>
/// <param name="center">The new center position</param>
/// <param name="length">Tne new length of this gizmo</param>
/// <param name="angle">The new angle of this gizmo</param>
public void Update(Vector2 center, float length, float angle)
{
this.center = center;
this.length = length;
this.angle = angle;
plane = Get2DPlane(center, angle);
planeOrtho = Get2DPlane(center, angle + 0.5f * (float)Mathf.PI);
Vector2 dir = new Vector2(planeOrtho.x, planeOrtho.y);
point1 = center + dir * length;
point2 = center - dir * length;
}
}
}

View File

@@ -0,0 +1,404 @@
using System;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using IDataProvider = UnityEngine.Rendering.LookDev.IDataProvider;
namespace UnityEditor.Rendering.LookDev
{
enum ShadowCompositionPass
{
MainView,
ShadowMask
}
enum CompositionFinal
{
First,
Second
}
class RenderTextureCache : IDisposable
{
const int k_PassPerViewCount = 3;
const int k_ViewCount = 2;
const int k_TextureCacheSize = k_PassPerViewCount * k_ViewCount;
//RenderTextures are packed this way:
//0: ViewIndex.First, ShadowCompositionPass.MainView
//1: ViewIndex.First, ShadowCompositionPass.ShadowMask
//2: CompositionFinal.First
//3: ViewIndex.Second, ShadowCompositionPass.MainView
//4: ViewIndex.Second, ShadowCompositionPass.ShadowMask
//5: CompositionFinal.Second
RenderTexture[] m_RTs = new RenderTexture[k_TextureCacheSize];
public RenderTexture this[ViewIndex index, ShadowCompositionPass passIndex]
{
get => m_RTs[computeIndex(index, passIndex)];
set => m_RTs[computeIndex(index, passIndex)] = value;
}
public RenderTexture this[CompositionFinal index]
{
get => m_RTs[computeIndex(index)];
set => m_RTs[computeIndex(index)] = value;
}
int computeIndex(ViewIndex index, ShadowCompositionPass passIndex)
=> (int)index * k_PassPerViewCount + (int)(passIndex);
int computeIndex(CompositionFinal index)
=> (k_PassPerViewCount - 1) + (int)(index) * k_PassPerViewCount;
void UpdateSize(int index, Rect rect, bool pixelPerfect, Camera renderingCamera, string renderDocName = "LookDevRT")
{
bool nullRect = rect.IsNullOrInverted();
GraphicsFormat format = SystemInfo.IsFormatSupported(GraphicsFormat.R16G16B16A16_SFloat, FormatUsage.Render)
? GraphicsFormat.R16G16B16A16_SFloat
: SystemInfo.GetGraphicsFormat(DefaultFormat.LDR);
if (m_RTs[index] != null && (nullRect || m_RTs[index].graphicsFormat != format))
{
m_RTs[index].Release();
UnityEngine.Object.DestroyImmediate(m_RTs[index]);
m_RTs[index] = null;
}
if (nullRect)
return;
int width = (int)rect.width;
int height = (int)rect.height;
if (m_RTs[index] == null)
{
m_RTs[index] = new RenderTexture(0, 0, 24, format);
m_RTs[index].name = renderDocName;
m_RTs[index].antiAliasing = 1;
m_RTs[index].hideFlags = HideFlags.HideAndDontSave;
}
if (m_RTs[index].width != width || m_RTs[index].height != height)
{
m_RTs[index].Release();
m_RTs[index].width = width;
m_RTs[index].height = height;
m_RTs[index].Create();
}
if (renderingCamera != null)
renderingCamera.targetTexture = m_RTs[index];
}
public void UpdateSize(Rect rect, ViewIndex index, bool pixelPerfect, Camera renderingCamera)
{
UpdateSize(computeIndex(index, ShadowCompositionPass.MainView), rect, pixelPerfect, renderingCamera, $"LookDevRT-{index}-MainView");
UpdateSize(computeIndex(index, ShadowCompositionPass.ShadowMask), rect, pixelPerfect, renderingCamera, $"LookDevRT-{index}-ShadowMask");
}
public void UpdateSize(Rect rect, CompositionFinal index, bool pixelPerfect, Camera renderingCamera)
=> UpdateSize(computeIndex(index), rect, pixelPerfect, renderingCamera, $"LookDevRT-Final-{index}");
bool m_Disposed = false;
public void Dispose()
{
if (m_Disposed)
return;
m_Disposed = true;
for (int index = 0; index < k_TextureCacheSize; ++index)
{
if (m_RTs[index] == null || m_RTs[index].Equals(null))
continue;
UnityEngine.Object.DestroyImmediate(m_RTs[index]);
m_RTs[index] = null;
}
}
}
class Compositer : IDisposable
{
public static readonly Color firstViewGizmoColor = new Color32(0, 154, 154, 255);
public static readonly Color secondViewGizmoColor = new Color32(255, 37, 4, 255);
static Material s_Material;
static Material material
{
get
{
if (s_Material == null || s_Material.Equals(null))
s_Material = new Material(Shader.Find("Hidden/LookDev/Compositor"));
return s_Material;
}
}
IViewDisplayer m_Displayer;
Context m_Contexts;
RenderTextureCache m_RenderTextures = new RenderTextureCache();
Renderer m_Renderer = new Renderer();
RenderingData[] m_RenderDataCache;
bool m_pixelPerfect;
bool m_Disposed;
public bool pixelPerfect
{
get => m_pixelPerfect;
set => m_Renderer.pixelPerfect = m_pixelPerfect = value;
}
Color m_AmbientColor = new Color(0.0f, 0.0f, 0.0f, 0.0f);
bool m_RenderDocAcquisitionRequested;
public Compositer(
IViewDisplayer displayer,
IDataProvider dataProvider,
StageCache stages)
{
m_Displayer = displayer;
m_RenderDataCache = new RenderingData[2]
{
new RenderingData() { stage = stages[ViewIndex.First] },
new RenderingData() { stage = stages[ViewIndex.Second] }
};
m_Displayer.OnRenderDocAcquisitionTriggered += RenderDocAcquisitionRequested;
m_Displayer.OnUpdateRequested += Render;
}
void RenderDocAcquisitionRequested()
=> m_RenderDocAcquisitionRequested = true;
void CleanUp()
{
for (int index = 0; index < 2; ++index)
{
m_RenderDataCache[index]?.Dispose();
m_RenderDataCache[index] = null;
}
m_RenderTextures.Dispose();
m_Displayer.OnRenderDocAcquisitionTriggered -= RenderDocAcquisitionRequested;
m_Displayer.OnUpdateRequested -= Render;
}
public void Dispose()
{
if (m_Disposed)
return;
m_Disposed = true;
CleanUp();
GC.SuppressFinalize(this);
}
~Compositer() => CleanUp();
public void Render()
{
// This can happen when entering/leaving playmode.
if (LookDev.dataProvider == null)
return;
m_Contexts = LookDev.currentContext;
//TODO: make integration EditorWindow agnostic!
if (UnityEditorInternal.RenderDoc.IsLoaded() && UnityEditorInternal.RenderDoc.IsSupported() && m_RenderDocAcquisitionRequested)
UnityEditorInternal.RenderDoc.BeginCaptureRenderDoc(m_Displayer as EditorWindow);
switch (m_Contexts.layout.viewLayout)
{
case Layout.FullFirstView:
RenderSingleAndOutput(ViewIndex.First);
break;
case Layout.FullSecondView:
RenderSingleAndOutput(ViewIndex.Second);
break;
case Layout.HorizontalSplit:
case Layout.VerticalSplit:
RenderSingleAndOutput(ViewIndex.First);
RenderSingleAndOutput(ViewIndex.Second);
break;
case Layout.CustomSplit:
RenderCompositeAndOutput();
break;
}
//TODO: make integration EditorWindow agnostic!
if (UnityEditorInternal.RenderDoc.IsLoaded() && UnityEditorInternal.RenderDoc.IsSupported() && m_RenderDocAcquisitionRequested)
UnityEditorInternal.RenderDoc.EndCaptureRenderDoc(m_Displayer as EditorWindow);
//stating that RenderDoc do not need to acquire anymore should
//allows to gather both view and composition in render doc at once
m_RenderDocAcquisitionRequested = false;
}
void AcquireDataForView(ViewIndex index, Rect viewport)
{
var renderingData = m_RenderDataCache[(int)index];
renderingData.viewPort = viewport;
ViewContext view = m_Contexts.GetViewContent(index);
m_RenderTextures.UpdateSize(renderingData.viewPort, index, m_Renderer.pixelPerfect, renderingData.stage.camera);
int debugMode = view.debug.viewMode;
if (debugMode != -1)
LookDev.dataProvider.UpdateDebugMode(debugMode);
renderingData.output = m_RenderTextures[index, ShadowCompositionPass.MainView];
renderingData.updater = view.camera;
m_Renderer.BeginRendering(renderingData, LookDev.dataProvider);
m_Renderer.Acquire(renderingData);
if (view.debug.shadow)
{
RenderTexture tmp = m_RenderTextures[index, ShadowCompositionPass.ShadowMask];
view.environment?.UpdateSunPosition(renderingData.stage.sunLight);
renderingData.stage.sunLight.intensity = 1f;
LookDev.dataProvider.GetShadowMask(ref tmp, renderingData.stage.runtimeInterface);
renderingData.stage.sunLight.intensity = 0f;
m_RenderTextures[index, ShadowCompositionPass.ShadowMask] = tmp;
}
m_Renderer.EndRendering(renderingData, LookDev.dataProvider);
if (debugMode != -1)
LookDev.dataProvider.UpdateDebugMode(-1);
}
void RenderSingleAndOutput(ViewIndex index)
{
Rect viewport = m_Displayer.GetRect((ViewCompositionIndex)index);
AcquireDataForView(index, viewport);
Compositing(viewport, (int)index, (CompositionFinal)index);
m_Displayer.SetTexture((ViewCompositionIndex)index, m_RenderTextures[(CompositionFinal)index]);
}
void RenderCompositeAndOutput()
{
Rect viewport = m_Displayer.GetRect(ViewCompositionIndex.Composite);
AcquireDataForView(ViewIndex.First, viewport);
AcquireDataForView(ViewIndex.Second, viewport);
Compositing(viewport, 2 /*split*/, CompositionFinal.First);
m_Displayer.SetTexture(ViewCompositionIndex.Composite, m_RenderTextures[CompositionFinal.First]);
}
void Compositing(Rect rect, int pass, CompositionFinal finalBufferIndex)
{
bool skipShadowComposition0 = !m_Contexts.GetViewContent(ViewIndex.First).debug.shadow;
bool skipShadowComposition1 = !m_Contexts.GetViewContent(ViewIndex.Second).debug.shadow;
if (rect.IsNullOrInverted()
|| (m_Contexts.layout.viewLayout != Layout.FullSecondView
&& (m_RenderTextures[ViewIndex.First, ShadowCompositionPass.MainView] == null
|| (!skipShadowComposition0
&& m_RenderTextures[ViewIndex.First, ShadowCompositionPass.ShadowMask] == null)))
|| (m_Contexts.layout.viewLayout != Layout.FullFirstView
&& (m_RenderTextures[ViewIndex.Second, ShadowCompositionPass.MainView] == null
|| (!skipShadowComposition1
&& m_RenderTextures[ViewIndex.Second, ShadowCompositionPass.ShadowMask] == null))))
{
m_RenderTextures[finalBufferIndex] = null;
return;
}
m_RenderTextures.UpdateSize(rect, finalBufferIndex, m_pixelPerfect, null);
ComparisonGizmoState gizmo = m_Contexts.layout.gizmoState;
Vector4 gizmoPosition = new Vector4(gizmo.center.x, gizmo.center.y, 0.0f, 0.0f);
Vector4 gizmoZoneCenter = new Vector4(gizmo.point2.x, gizmo.point2.y, 0.0f, 0.0f);
Vector4 gizmoThickness = new Vector4(ComparisonGizmoState.thickness, ComparisonGizmoState.thicknessSelected, 0.0f, 0.0f);
Vector4 gizmoCircleRadius = new Vector4(ComparisonGizmoState.circleRadius, ComparisonGizmoState.circleRadiusSelected, 0.0f, 0.0f);
Environment env0 = m_Contexts.GetViewContent(ViewIndex.First).environment;
Environment env1 = m_Contexts.GetViewContent(ViewIndex.Second).environment;
float exposureValue0 = env0?.exposure ?? 0f;
float exposureValue1 = env1?.exposure ?? 0f;
float dualViewBlendFactor = gizmo.blendFactor;
float isCurrentlyLeftEditting = m_Contexts.layout.lastFocusedView == ViewIndex.First ? 1f : -1f;
float dragAndDropContext = 0f; //1f left, -1f right, 0f neither
float toneMapEnabled = -1f; //1f true, -1f false
float shadowMultiplier0 = skipShadowComposition0 ? -1f : 1.0f;
float shadowMultiplier1 = skipShadowComposition1 ? -1f : 1.0f;
Color shadowColor0 = env0?.shadowColor ?? Color.white;
Color shadowColor1 = env1?.shadowColor ?? Color.white;
//TODO: handle shadow not at compositing step but in rendering
Texture texMainView0 = m_RenderTextures[ViewIndex.First, ShadowCompositionPass.MainView];
Texture texShadowsMask0 = m_RenderTextures[ViewIndex.First, ShadowCompositionPass.ShadowMask];
Texture texMainView1 = m_RenderTextures[ViewIndex.Second, ShadowCompositionPass.MainView];
Texture texShadowsMask1 = m_RenderTextures[ViewIndex.Second, ShadowCompositionPass.ShadowMask];
Vector4 compositingParams = new Vector4(dualViewBlendFactor, exposureValue0, exposureValue1, isCurrentlyLeftEditting);
Vector4 compositingParams2 = new Vector4(dragAndDropContext, toneMapEnabled, shadowMultiplier0, shadowMultiplier1);
// Those could be tweakable for the neutral tonemapper, but in the case of the LookDev we don't need that
const float k_BlackIn = 0.02f;
const float k_WhiteIn = 10.0f;
const float k_BlackOut = 0.0f;
const float k_WhiteOut = 10.0f;
const float k_WhiteLevel = 5.3f;
const float k_WhiteClip = 10.0f;
const float k_DialUnits = 20.0f;
const float k_HalfDialUnits = k_DialUnits * 0.5f;
const float k_GizmoRenderMode = 4f; //display all
// converting from artist dial units to easy shader-lerps (0-1)
//TODO: to compute one time only
Vector4 tonemapCoeff1 = new Vector4((k_BlackIn * k_DialUnits) + 1.0f, (k_BlackOut * k_HalfDialUnits) + 1.0f, (k_WhiteIn / k_DialUnits), (1.0f - (k_WhiteOut / k_DialUnits)));
Vector4 tonemapCoeff2 = new Vector4(0.0f, 0.0f, k_WhiteLevel, k_WhiteClip / k_HalfDialUnits);
const float k_ReferenceScale = 1080.0f;
Vector4 screenRatio = new Vector4(rect.width / k_ReferenceScale, rect.height / k_ReferenceScale, rect.width, rect.height);
RenderTexture oldActive = RenderTexture.active;
RenderTexture.active = m_RenderTextures[finalBufferIndex];
material.SetTexture("_Tex0MainView", texMainView0);
material.SetTexture("_Tex0Shadows", texShadowsMask0);
material.SetColor("_ShadowColor0", shadowColor0);
material.SetTexture("_Tex1MainView", texMainView1);
material.SetTexture("_Tex1Shadows", texShadowsMask1);
material.SetColor("_ShadowColor1", shadowColor1);
material.SetVector("_CompositingParams", compositingParams);
material.SetVector("_CompositingParams2", compositingParams2);
material.SetColor("_FirstViewColor", firstViewGizmoColor);
material.SetColor("_SecondViewColor", secondViewGizmoColor);
material.SetVector("_GizmoPosition", gizmoPosition);
material.SetVector("_GizmoZoneCenter", gizmoZoneCenter);
material.SetVector("_GizmoSplitPlane", gizmo.plane);
material.SetVector("_GizmoSplitPlaneOrtho", gizmo.planeOrtho);
material.SetFloat("_GizmoLength", gizmo.length);
material.SetVector("_GizmoThickness", gizmoThickness);
material.SetVector("_GizmoCircleRadius", gizmoCircleRadius);
material.SetFloat("_BlendFactorCircleRadius", ComparisonGizmoState.blendFactorCircleRadius);
material.SetFloat("_GetBlendFactorMaxGizmoDistance", gizmo.blendFactorMaxGizmoDistance);
material.SetFloat("_GizmoRenderMode", k_GizmoRenderMode);
material.SetVector("_ScreenRatio", screenRatio);
material.SetVector("_ToneMapCoeffs1", tonemapCoeff1);
material.SetVector("_ToneMapCoeffs2", tonemapCoeff2);
material.SetPass(pass);
Renderer.DrawFullScreenQuad(new Rect(0, 0, rect.width, rect.height));
RenderTexture.active = oldActive;
}
public ViewIndex GetViewFromComposition(Vector2 localCoordinate)
{
Rect compositionRect = m_Displayer.GetRect(ViewCompositionIndex.Composite);
Vector2 normalizedLocalCoordinate = ComparisonGizmoController.GetNormalizedCoordinates(localCoordinate, compositionRect);
switch (m_Contexts.layout.viewLayout)
{
case Layout.CustomSplit:
return Vector3.Dot(new Vector3(normalizedLocalCoordinate.x, normalizedLocalCoordinate.y, 1), m_Contexts.layout.gizmoState.plane) >= 0
? ViewIndex.First
: ViewIndex.Second;
default:
throw new Exception("GetViewFromComposition call when not inside a Composition");
}
}
}
}

View File

@@ -0,0 +1,460 @@
Shader "Hidden/LookDev/Compositor"
{
Properties
{
_Tex0MainView("Main View", 2D) = "white" {}
_Tex0Shadows("First View shadow mask", 2D) = "white" {}
_ShadowColor0("Shadow Color for first view", Color) = (1.0, 1.0, 1.0, 1.0)
_Tex1MainView("Second View", 2D) = "white" {}
_Tex1Shadows("Second View shadow mask", 2D) = "white" {}
_ShadowColor1("Shadow Color for second view", Color) = (1.0, 1.0, 1.0, 1.0)
_CompositingParams("Blend Factor, exposure for first and second view, and current selected side", Vector) = (0.0, 1.0, 1.0, 1.0)
_CompositingParams2("Drag and drop zone and shadow multipliers", Vector) = (0.0, 1.0, 1.0, 1.0) // Drag and Drop zone Left == 1.0, Right == -1.0, None == 0.0
_FirstViewColor("Gizmo Color for first view", Color) = (0.5, 0.5, 0.5, 0.5)
_SecondViewColor("Gizmo Color for second view", Color) = (0.5, 0.5, 0.5, 0.5)
_GizmoPosition("Position of split view gizmo", Vector) = (0.5, 0.5, 0.0, 0.0)
_GizmoZoneCenter("Center of Zone view gizmo", Vector) = (0.5, 0.5, 0.0, 0.0)
_GizmoSplitPlane("2D plane of the gizmo", Vector) = (1.0, 1.0, 0.0, 0.0)
_GizmoSplitPlaneOrtho("2D plane orthogonal to the gizmo", Vector) = (1.0, 1.0, 0.0, 0.0)
_GizmoLength("Gizmo Length", Float) = 0.2
_GizmoThickness("Gizmo Thickness", Vector) = (0.01, 0.08, 0.0, 0.0)
_GizmoCircleRadius("Gizmo extremities radius", Vector) = (0.05, 0.4, 0.0, 0.0)
_GizmoRenderMode("Render gizmo mode", Float) = 0.0
_GetBlendFactorMaxGizmoDistance("Distance on the gizmo where the blend circle stops", Float) = 0.2
_BlendFactorCircleRadius("Visual radius of the blend factor gizmo", Float) = 0.01
_ScreenRatio("Screen ratio", Vector) = (1.0, 1.0, 0.0, 0.0) // xy screen ratio, zw screen size
_ToneMapCoeffs1("Parameters for neutral tonemap", Vector) = (0.0, 0.0, 0.0, 0.0)
_ToneMapCoeffs2("Parameters for neutral tonemap", Vector) = (0.0, 0.0, 0.0, 0.0)
}
CGINCLUDE
#include "UnityCG.cginc"
#pragma vertex vert
// Enum matching GizmoOperationType in LookDevViews.cs
#define kNone 0.0f
#define kTranslation 1.0f
#define kRotationZone1 2.0f
#define kRotationZone2 3.0f
#define kAll 4.0f
sampler2D _Tex0MainView;
sampler2D _Tex0Shadows;
float4 _ShadowColor0;
sampler2D _Tex1MainView;
sampler2D _Tex1Shadows;
float4 _ShadowColor1;
float4 _CompositingParams; // x BlendFactor, yz ExposureValue (first/second view), w current selected side
float4 _CompositingParams2; // x current drag context, y apply tonemap (bool), z shadow multiplier
float4 _FirstViewColor;
float4 _SecondViewColor;
float4 _GizmoPosition;
float4 _GizmoZoneCenter;
float4 _GizmoThickness;
float4 _GizmoCircleRadius;
float4 _GizmoSplitPlane;
float4 _GizmoSplitPlaneOrtho;
float _GizmoLength;
float _GizmoRenderMode;
float _GetBlendFactorMaxGizmoDistance;
float _BlendFactorCircleRadius;
float4 _ScreenRatio;
float4 _ToneMapCoeffs1;
float4 _ToneMapCoeffs2;
float4 _Tex0MainView_ST;
#define ShadowMultiplier0 _CompositingParams2.z
#define ShadowMultiplier1 _CompositingParams2.w
#define ExposureValue1 _CompositingParams.y
#define ExposureValue2 _CompositingParams.z
#define InBlack _ToneMapCoeffs1.x
#define OutBlack _ToneMapCoeffs1.y
#define InWhite _ToneMapCoeffs1.z
#define OutWhite _ToneMapCoeffs1.w
#define WhiteLevel _ToneMapCoeffs2.z
#define WhiteClip _ToneMapCoeffs2.w
struct appdata_t
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
};
struct v2f
{
float2 texcoord : TEXCOORD0;
float4 vertex : SV_POSITION;
};
float DistanceToSplit(float2 pos, float3 splitPlane)
{
return dot(float3(pos, 1), splitPlane);
}
bool IsInsideGizmo(float2 normalizedCoord, float absDistanceToPlane, float distanceFromCenter, float side, float3 orthoPlane, float gizmoCircleRadius, float gizmoThickness, out float outSmoothing, float mode)
{
bool result = false;
outSmoothing = 0.0;
if (absDistanceToPlane < gizmoCircleRadius) // First "thick" bar, as large as the radius at extremities.
{
if (distanceFromCenter < (_GizmoLength + gizmoCircleRadius))
{
// side < 0 is cyan circle, side > 0 is orange widget
if (mode == kAll ||
(mode == kRotationZone1 && side > 0) ||
(mode == kRotationZone2 && side < 0))
{
if (distanceFromCenter >= (_GizmoLength - gizmoCircleRadius)) // Inside circle at the extremities ?
{
float2 circleCenter = _GizmoPosition.xy + side * orthoPlane.xy * _GizmoLength;
float d = length(normalizedCoord - circleCenter);
if (d <= gizmoCircleRadius)
{
outSmoothing = smoothstep(1.0, 0.8, d / gizmoCircleRadius);
result = true;
}
}
}
if (mode == kAll || mode == kTranslation)
{
if (absDistanceToPlane < gizmoThickness && distanceFromCenter < _GizmoLength)
{
outSmoothing = max(outSmoothing, smoothstep(1.0, 0.0, absDistanceToPlane / gizmoThickness));
result = true;
}
}
}
}
return result;
}
float4 GetGizmoColor(float2 normalizedCoord, float3 splitPlane, float3 orthoPlane)
{
float distanceToPlane = DistanceToSplit(normalizedCoord, splitPlane);
float absDistanceToPlane = abs(distanceToPlane);
float distanceFromCenter = length(normalizedCoord.xy - _GizmoPosition.xy);
float distanceToOrtho = DistanceToSplit(normalizedCoord, orthoPlane);
float4 result = float4(0.0, 0.0, 0.0, 0.0);
float side = 0.0;
if (distanceToOrtho > 0.0)
{
result.rgb = _FirstViewColor.rgb;
side = 1.0;
}
else
{
result.rgb = _SecondViewColor.rgb;
side = -1.0;
}
result.a = 0.0;
// "normal" gizmo
float smoothing = 1.0;
if (IsInsideGizmo(normalizedCoord, absDistanceToPlane, distanceFromCenter, side, orthoPlane, _GizmoCircleRadius.x, _GizmoThickness.x, smoothing, kAll))
{
result.a = 1.0 * smoothing;
}
// large gizmo when in translation mode
if (IsInsideGizmo(normalizedCoord, absDistanceToPlane, distanceFromCenter, side, orthoPlane, _GizmoCircleRadius.y, _GizmoThickness.y, smoothing, _GizmoRenderMode))
{
result.a = max(result.a, 0.25 * smoothing);
}
// Blend factor selection disc
float2 blendCircleCenter = _GizmoPosition.xy - _CompositingParams.x * orthoPlane.xy * _GetBlendFactorMaxGizmoDistance;
float distanceToBlendCircle = length(normalizedCoord.xy - blendCircleCenter);
if (distanceToBlendCircle < _BlendFactorCircleRadius)
{
float alpha = smoothstep(1.0, 0.6, distanceToBlendCircle / _BlendFactorCircleRadius);
result = lerp(result, float4(1.0, 1.0, 1.0, alpha), alpha);
}
// Display transparent disc if near the center where the blend factor selection disc will automatically snap back
if (abs(_CompositingParams.x) < _GizmoCircleRadius.y / _GetBlendFactorMaxGizmoDistance)
{
if (distanceFromCenter < _BlendFactorCircleRadius)
{
float alpha = smoothstep(1.0, 0.6, distanceFromCenter / _BlendFactorCircleRadius) * 0.75;
result = lerp(result, float4(1.0, 1.0, 1.0, alpha), alpha);
}
}
return result;
}
float GetZoneViewFeedbackCircleFactor(float2 normalizedCoord, float radius, float circleSize)
{
float distanceToCenter = abs(length(_GizmoZoneCenter.xy - normalizedCoord) - radius);
return saturate((circleSize - distanceToCenter) / circleSize);
}
float ComputeBorderFactor(float borderSize, float2 screenPos, bool sideBySideView)
{
float4 borderSize4 = float4(borderSize, borderSize, borderSize, borderSize);
float4 distanceToBorder = float4(screenPos.x, screenPos.y, abs(_ScreenRatio.z - screenPos.x), abs(_ScreenRatio.w - screenPos.y));
float4 factors = saturate((borderSize4 - distanceToBorder) / borderSize4); // Lerp from 1.0 to 0.0 alpha from screen border to border size
float factor = max(factors.x, max(factors.y, max(factors.z, factors.w)));
// Add middle of the screen for side by side view
if (sideBySideView)
{
float distanceToCenterLine = abs(_ScreenRatio.z * 0.5 - screenPos.x);
float factorForCenterLine = saturate((borderSize - distanceToCenterLine) / borderSize);
factor = max(factor, factorForCenterLine);
}
return factor;
}
float ComputeSelectedSideColorFactor(float side, float2 screenPos, float2 normalizedCoord, bool sideBySideView, bool zoneView)
{
float borderSize = 2.0;
bool selectedSide = side * _CompositingParams.w > 0.0;
float factor = ComputeBorderFactor(borderSize, screenPos, sideBySideView);
// Add circle for zone view
if (zoneView)
{
float selectionCircleFeedbackFactor = GetZoneViewFeedbackCircleFactor(normalizedCoord, _GizmoCircleRadius.y, 0.002);
factor = max(factor, selectionCircleFeedbackFactor);
}
// If not on the selected side, make it more transparent
if (!selectedSide)
{
factor = factor * 0.2;
}
return factor;
}
float4 ComputeDragColorFactor(float side, float2 screenPos, float2 normalizedCoord, bool sideBySideView, bool zoneView)
{
float factor = 0;
float borderSize = 40.0;
bool sideIsDragZone = (side > 0.0 && _CompositingParams2.x > 0.0) || (side < 0.0 && _CompositingParams2.x < 0.0);
if (sideIsDragZone)
{
factor = ComputeBorderFactor(borderSize, screenPos, sideBySideView);
// Add circle for zone view
if (zoneView && side < 0.0)
{
float feedbackRadius = _GizmoLength * 2.0 * 0.3; // make it proprtional to selection zone
factor = max(factor, GetZoneViewFeedbackCircleFactor(normalizedCoord, _GizmoLength * 2.0, feedbackRadius));
}
factor = pow(factor, 8) * 0.7; // Casimir magics values for optimum fadeout :)
}
return factor;
}
float4 ComputeFeedbackColor(float4 inputColor, float side, float2 screenPos, float2 normalizedCoord, bool sideBySideView, bool zoneView)
{
float factor = ComputeSelectedSideColorFactor(side, screenPos, normalizedCoord, sideBySideView, zoneView);
factor = max(factor, ComputeDragColorFactor(side, screenPos, normalizedCoord, sideBySideView, zoneView));
float4 result = float4(0.0, 0.0, 0.0, 0.0);
if (side > 0.0)
{
result = lerp(inputColor, _FirstViewColor, factor);
}
else
{
result = lerp(inputColor, _SecondViewColor, factor);
}
return result;
}
v2f vert(appdata_t IN)
{
v2f OUT;
OUT.vertex = UnityObjectToClipPos(IN.vertex);
OUT.texcoord = TRANSFORM_TEX(IN.texcoord, _Tex0MainView);
return OUT;
}
float3 evalCurve(float3 x, float A, float B, float C, float D, float E, float F)
{
return ((x*(A*x + C*B) + D*E) / (x*(A*x + B) + D*F)) - E / F;
}
float3 applyTonemapFilmicAD(float3 linearColor)
{
float blackRatio = InBlack / OutBlack;
float whiteRatio = InWhite / OutWhite;
// blend tunable coefficients
float B = lerp(0.57, 0.37, blackRatio);
float C = lerp(0.01, 0.24, whiteRatio);
float D = lerp(0.02, 0.20, blackRatio);
// constants
float A = 0.2;
float E = 0.02;
float F = 0.30;
// eval and correct for white point
float3 whiteScale = 1.0f / evalCurve(WhiteLevel, A, B, C, D, E, F);
float3 curr = evalCurve(linearColor *whiteScale, A, B, C, D, E, F);
return curr*whiteScale;
}
float3 remapWhite(float3 inPixel, float whitePt)
{
// var breakout for readability
const float inBlack = 0;
const float outBlack = 0;
float inWhite = whitePt;
const float outWhite = 1;
// remap input range to output range
float3 outPixel = ((inPixel.rgb) - inBlack.xxx) / (inWhite.xxx - inBlack.xxx) * (outWhite.xxx - outBlack.xxx) + outBlack.xxx;
return (outPixel.rgb);
}
float3 NeutralTonemap(float3 x)
{
float3 finalColor = applyTonemapFilmicAD(x); // curve (dynamic coeffs differ per level)
finalColor = remapWhite(finalColor, WhiteClip); // post-curve white point adjustment
finalColor = saturate(finalColor);
return finalColor;
}
float3 ApplyToneMap(float3 color)
{
if (_CompositingParams2.y > 0.0)
{
return NeutralTonemap(color);
}
else
{
return saturate(color);
}
}
float3 ComputeColor(sampler2D texNormal, sampler2D texShadowMask, float shadowMultiplier, float4 shadowColor, float2 texcoord)
{
// Explanation of how this work:
// To simulate the shadow of a directional light, we want to interpolate between two environments. One environment being the regular cubemap and the other a darkened version of the same cubemap
// To create the lerp mask we render the scene with a white diffuse material and a single shadow casting directional light.
// This will create a mask where the shadowed area is 0 and the lit area is 1 with a smooth NDotL transition in-between.
// However, the DNotL will create an unwanted darkening of the scene (it's not actually part of the lighting equation)
// so we sort it in order to avoid too much darkening.
float3 color = tex2D(texNormal, texcoord).rgb;
if (shadowMultiplier < 0.0)
{
// no need to composite as we do not want shadow in this case
return color;
}
else
{
float3 shadowMask = sqrt(tex2D(texShadowMask, texcoord).rgb);
return lerp(color * shadowColor.rgb * shadowMultiplier, color, saturate(shadowMask.r));
}
}
ENDCG
SubShader
{
Tags
{
"ForceSupported" = "True"
}
Lighting Off
Cull Off
ZTest Always
ZWrite Off
Blend One Zero
// Single view 1
Pass
{
CGPROGRAM
#pragma fragment frag
#pragma target 3.0
float4 frag(float2 texcoord : TEXCOORD0,
UNITY_VPOS_TYPE vpos : VPOS) : SV_Target
{
float4 color = float4(ComputeColor(_Tex0MainView, _Tex0Shadows, ShadowMultiplier0, _ShadowColor0, texcoord) * exp2(ExposureValue1), 1.0);
color.rgb = ApplyToneMap(color.rgb);
color = ComputeFeedbackColor(color, 1.0, vpos.xy, float2(0.0, 0.0), false, false);
return color;
}
ENDCG
}
// Single view 2
Pass
{
CGPROGRAM
#pragma fragment frag
#pragma target 3.0
float4 frag(float2 texcoord : TEXCOORD0,
UNITY_VPOS_TYPE vpos : VPOS) : SV_Target
{
float4 color = float4(ComputeColor(_Tex1MainView, _Tex1Shadows, ShadowMultiplier1, _ShadowColor1, texcoord) * exp2(ExposureValue2), 1.0);
color.rgb = ApplyToneMap(color.rgb);
color = ComputeFeedbackColor(color, -1.0, vpos.xy, float2(0.0, 0.0), false, false);
return color;
}
ENDCG
}
// split
Pass
{
CGPROGRAM
#pragma fragment frag
#pragma target 3.0
float4 frag(float2 texcoord : TEXCOORD0,
UNITY_VPOS_TYPE vpos : VPOS) : SV_Target
{
float3 color1 = ComputeColor(_Tex0MainView, _Tex0Shadows, ShadowMultiplier0, _ShadowColor0, texcoord) * exp2(ExposureValue1);
float3 color2 = ComputeColor(_Tex1MainView, _Tex1Shadows, ShadowMultiplier1, _ShadowColor1, texcoord) * exp2(ExposureValue2);
float2 normalizedCoord = ((texcoord * 2.0 - 1.0) * _ScreenRatio.xy);
float side = DistanceToSplit(normalizedCoord, _GizmoSplitPlane) < 0.0f ? -1.0f : 1.0f;
float blendFactor = 0.0f;
if (side < 0.0)
{
blendFactor = 1.0 - saturate(side * _CompositingParams.x);
}
else
{
blendFactor = saturate(side * _CompositingParams.x);
}
float4 finalColor = float4(lerp(color1, color2, blendFactor), 1.0);
finalColor.rgb = ApplyToneMap(finalColor.rgb);
float4 gizmoColor = GetGizmoColor(normalizedCoord, _GizmoSplitPlane, _GizmoSplitPlaneOrtho);
finalColor = lerp(finalColor, gizmoColor, gizmoColor.a);
finalColor = ComputeFeedbackColor(finalColor, side, vpos.xy, float2(0.0, 0.0), false, false);
return finalColor;
}
ENDCG
}
}
}

View File

@@ -0,0 +1,497 @@
using System;
using UnityEngine;
using System.Collections.Generic;
using System.Collections;
namespace UnityEditor.Rendering.LookDev
{
/// <summary>
/// Different working views in LookDev
/// </summary>
public enum ViewIndex
{
/// <summary>First view</summary>
First,
/// <summary>Second view</summary>
Second
};
/// <summary>
/// Same as <see cref="ViewIndex"/> plus a compound value
/// </summary>
public enum ViewCompositionIndex
{
/// <summary>First view</summary>
First = ViewIndex.First,
/// <summary>Second view</summary>
Second = ViewIndex.Second,
/// <summary>Composite view (Several view on screen)</summary>
Composite
};
// /!\ WARNING: these value name are used as uss file too.
// if your rename here, rename in the uss too.
/// <summary>
/// Different layout supported in LookDev
/// </summary>
public enum Layout
{
/// <summary>First view display fully</summary>
FullFirstView,
/// <summary>Second view display fully</summary>
FullSecondView,
/// <summary>First and second views displayed splitted horizontally</summary>
HorizontalSplit,
/// <summary>First and second views displayed splitted vertically</summary>
VerticalSplit,
/// <summary>First and second views displayed with stacking and orientation customizable split</summary>
CustomSplit
}
/// <summary>
/// Status of the side panel of the LookDev window
/// </summary>
public enum SidePanel
{
/// <summary>No side panel</summary>
None = -1,
/// <summary>Environment side panel</summary>
Environment,
/// <summary>Debug side panel</summary>
Debug,
}
/// <summary>
/// The target views of the debug panel
/// </summary>
public enum TargetDebugView
{
/// <summary>First Debug view</summary>
First,
/// <summary>Both Debug view</summary>
Both,
/// <summary>Second Debug view</summary>
Second
};
/// <summary>
/// Class containing all data used by the LookDev Window to render
/// </summary>
[System.Serializable]
public class Context : ScriptableObject, IDisposable
{
[SerializeField]
string m_EnvironmentLibraryGUID = ""; //Empty GUID
[SerializeField]
bool m_CameraSynced = true;
EnvironmentLibrary m_EnvironmentLibrary;
/// <summary>The currently used Environment</summary>
public EnvironmentLibrary environmentLibrary
{
get
{
//check if asset deleted by user
if (m_EnvironmentLibrary != null && AssetDatabase.Contains(m_EnvironmentLibrary))
return m_EnvironmentLibrary;
if (!String.IsNullOrEmpty(m_EnvironmentLibraryGUID))
{
//user deleted the EnvironmentLibrary asset
m_EnvironmentLibraryGUID = ""; //Empty GUID
LookDev.currentEnvironmentDisplayer.Repaint();
}
return null;
}
private set => m_EnvironmentLibrary = value;
}
/// <summary>The currently used layout</summary>
[field: SerializeField]
public LayoutContext layout { get; private set; } = new LayoutContext();
/// <summary>
/// State if both views camera movement are synced or not
/// </summary>
public bool cameraSynced
{
get => m_CameraSynced;
set
{
if (m_CameraSynced ^ value)
{
if (value)
EditorApplication.update += SynchronizeCameraStates;
else
EditorApplication.update -= SynchronizeCameraStates;
m_CameraSynced = value;
}
}
}
[SerializeField]
ViewContext[] m_Views = new ViewContext[2]
{
new ViewContext(),
new ViewContext()
};
/// <summary>
/// Helper class to iterate on views
/// </summary>
public struct ViewIterator : IEnumerable<ViewContext>
{
ViewContext[] m_Views;
internal ViewIterator(ViewContext[] views)
=> m_Views = views;
/// <summary>
/// Helper function to enumerates on ViewContexts
/// </summary>
/// <returns>Enumerator on ViewContext</returns>
IEnumerator IEnumerable.GetEnumerator()
=> m_Views.GetEnumerator();
/// <summary>
/// Helper function to enumerates on ViewContexts
/// </summary>
/// <returns>Enumerator on ViewContext</returns>
IEnumerator<ViewContext> IEnumerable<ViewContext>.GetEnumerator()
=> ((IEnumerable<ViewContext>)m_Views).GetEnumerator();
}
/// <summary>
/// Helper function to get ViewIterator on ViewContexts
/// </summary>
public ViewIterator viewContexts
=> new ViewIterator(m_Views);
/// <summary>
/// Get datas relative to a view
/// </summary>
/// <param name="index">The view index to look at</param>
/// <returns>Datas for the selected view</returns>
public ViewContext GetViewContent(ViewIndex index)
=> m_Views[(int)index];
internal void Init()
{
LoadEnvironmentLibraryFromGUID();
//recompute non serialized computes states
layout.gizmoState.Init();
if (cameraSynced)
EditorApplication.update += SynchronizeCameraStates;
}
/// <summary>Update the environment library used.</summary>
/// <param name="library">The new EnvironmentLibrary</param>
public void UpdateEnvironmentLibrary(EnvironmentLibrary library)
{
m_EnvironmentLibraryGUID = "";
environmentLibrary = null;
if (library == null || library.Equals(null))
return;
m_EnvironmentLibraryGUID = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(library));
environmentLibrary = library;
}
void LoadEnvironmentLibraryFromGUID()
{
environmentLibrary = null;
GUID storedGUID;
GUID.TryParse(m_EnvironmentLibraryGUID, out storedGUID);
if (storedGUID.Empty())
return;
string path = AssetDatabase.GUIDToAssetPath(m_EnvironmentLibraryGUID);
environmentLibrary = AssetDatabase.LoadAssetAtPath<EnvironmentLibrary>(path);
}
/// <summary>
/// Synchronize cameras from both view using data from the baseCameraState
/// </summary>
/// <param name="baseCameraState">The <see cref="ViewIndex"/> to be used as reference</param>
public void SynchronizeCameraStates(ViewIndex baseCameraState)
{
switch (baseCameraState)
{
case ViewIndex.First:
m_Views[1].camera.SynchronizeFrom(m_Views[0].camera);
break;
case ViewIndex.Second:
m_Views[0].camera.SynchronizeFrom(m_Views[1].camera);
break;
default:
throw new System.ArgumentException("Unknow ViewIndex given in parameter.");
}
}
void SynchronizeCameraStates()
=> SynchronizeCameraStates(layout.lastFocusedView);
/// <summary>
/// Change focused view.
/// Focused view is the base view to copy data when syncing views' cameras
/// </summary>
/// <param name="index">The index of the view</param>
public void SetFocusedCamera(ViewIndex index)
=> layout.lastFocusedView = index;
private bool disposedValue = false; // To detect redundant calls
/// <summary>Disposable behaviour</summary>
void IDisposable.Dispose()
{
if (!disposedValue)
{
if (cameraSynced)
EditorApplication.update -= SynchronizeCameraStates;
disposedValue = true;
}
}
internal bool HasLibraryAssetChanged(EnvironmentLibrary environmentLibrary)
{
if (environmentLibrary == null)
return !String.IsNullOrEmpty(m_EnvironmentLibraryGUID);
return m_EnvironmentLibraryGUID != AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(environmentLibrary));
}
internal void FullReimportEnvironmentLibrary()
{
if (environmentLibrary == null)
return;
// refresh AssetDatabase in case of undo/redo creating/destructing environment subasset
string libraryPath = AssetDatabase.GetAssetPath(environmentLibrary);
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(environmentLibrary), ImportAssetOptions.DontDownloadFromCacheServer | ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate | ImportAssetOptions.ImportRecursive);
UpdateEnvironmentLibrary(AssetDatabase.LoadAssetAtPath<EnvironmentLibrary>(libraryPath));
EditorUtility.SetDirty(environmentLibrary);
}
}
/// <summary>
/// Data regarding the layout currently used in LookDev
/// </summary>
[System.Serializable]
public class LayoutContext
{
/// <summary>The layout used</summary>
public Layout viewLayout;
/// <summary>The last focused view</summary>
public ViewIndex lastFocusedView = ViewIndex.First;
/// <summary>The state of the side panel</summary>
public SidePanel showedSidePanel;
/// <summary>The view to change when manipulating the Debug side panel</summary>
[NonSerialized]
public TargetDebugView debugPanelSource = TargetDebugView.Both;
[SerializeField]
internal ComparisonGizmoState gizmoState = new ComparisonGizmoState();
internal bool isSimpleView => viewLayout == Layout.FullFirstView || viewLayout == Layout.FullSecondView;
internal bool isMultiView => viewLayout == Layout.HorizontalSplit || viewLayout == Layout.VerticalSplit;
internal bool isCombinedView => viewLayout == Layout.CustomSplit;
}
/// <summary>
/// Data container containing content of a view
/// </summary>
[System.Serializable]
public class ViewContext
{
/// <summary>The position and rotation of the camera</summary>
[field: SerializeField]
public CameraState camera { get; private set; } = new CameraState();
/// <summary>The currently viewed debugState</summary>
public DebugContext debug { get; private set; } = new DebugContext();
//Environment asset, sub-asset (under a library) or cubemap
[SerializeField]
string m_EnvironmentGUID = ""; //Empty GUID
/// <summary>
/// Check if an Environment is registered for this view.
/// The result will be accurate even if the Environment have not been reloaded yet.
/// </summary>
public bool hasEnvironment => !String.IsNullOrEmpty(m_EnvironmentGUID);
/// <summary>The currently used Environment</summary>
public Environment environment { get; private set; }
[SerializeField]
string viewedObjectAssetGUID = ""; //Empty GUID
// Careful here: we want to keep it while reloading script.
// But from one unity editor to an other, ID are not kept.
// So, only use it when reloading from script update.
[SerializeField]
int viewedObjecHierarchytInstanceID;
/// <summary>
/// Check if an Environment is registered for this view.
/// The result will be accurate even if the object have not been reloaded yet.
/// </summary>
public bool hasViewedObject =>
!String.IsNullOrEmpty(viewedObjectAssetGUID)
|| viewedObjecHierarchytInstanceID != 0;
/// <summary>Reference to the object given for instantiation.</summary>
public GameObject viewedObjectReference { get; private set; }
/// <summary>
/// The currently displayed instance of <see cref="viewedObjectReference"/>.
/// It will be instantiated when pushing changes to renderer.
/// See <see cref="LookDev.SaveContextChangeAndApply(ViewIndex)"/>
/// </summary>
public GameObject viewedInstanceInPreview { get; internal set; }
/// <summary>Update the environment used.</summary>
/// <param name="environmentOrCubemapAsset">
/// The new <see cref="Environment"/> to use.
/// Or the <see cref="Cubemap"/> to use to build a new one.
/// Other types will raise an ArgumentException.
/// </param>
public void UpdateEnvironment(UnityEngine.Object environmentOrCubemapAsset)
{
m_EnvironmentGUID = "";
environment = null;
if (environmentOrCubemapAsset == null || environmentOrCubemapAsset.Equals(null))
return;
if (!(environmentOrCubemapAsset is Environment)
&& !(environmentOrCubemapAsset is Cubemap))
throw new System.ArgumentException("Only Environment or Cubemap accepted for environmentOrCubemapAsset parameter");
string GUID;
long localIDInFile;
AssetDatabase.TryGetGUIDAndLocalFileIdentifier(environmentOrCubemapAsset, out GUID, out localIDInFile);
m_EnvironmentGUID = $"{GUID},{localIDInFile}";
if (environmentOrCubemapAsset is Environment)
environment = environmentOrCubemapAsset as Environment;
else //Cubemap
environment = Environment.GetTemporaryEnvironmentForCubemap(environmentOrCubemapAsset as Cubemap);
}
void LoadEnvironmentFromGUID()
{
environment = null;
GUID storedGUID;
string[] GUIDAndLocalIDInFile = m_EnvironmentGUID.Split(new[] { ',' });
GUID.TryParse(GUIDAndLocalIDInFile[0], out storedGUID);
if (storedGUID.Empty())
return;
long localIDInFile = GUIDAndLocalIDInFile.Length < 2 ? 0L : long.Parse(GUIDAndLocalIDInFile[1]);
string path = AssetDatabase.GUIDToAssetPath(GUIDAndLocalIDInFile[0]);
Type savedType = AssetDatabase.GetMainAssetTypeAtPath(path);
if (savedType == typeof(EnvironmentLibrary))
{
object[] loaded = AssetDatabase.LoadAllAssetsAtPath(path);
for (int i = 0; i < loaded.Length; ++i)
{
string garbage;
long testedLocalIndex;
if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier((UnityEngine.Object)loaded[i], out garbage, out testedLocalIndex)
&& testedLocalIndex == localIDInFile)
{
environment = loaded[i] as Environment;
break;
}
}
}
else if (savedType == typeof(Environment))
environment = AssetDatabase.LoadAssetAtPath<Environment>(path);
else if (savedType == typeof(Cubemap))
{
Cubemap cubemap = AssetDatabase.LoadAssetAtPath<Cubemap>(path);
environment = Environment.GetTemporaryEnvironmentForCubemap(cubemap);
}
}
/// <summary>Update the object reference used for instantiation.</summary>
/// <param name="viewedObject">The new reference.</param>
public void UpdateViewedObject(GameObject viewedObject)
{
viewedObjectAssetGUID = "";
viewedObjecHierarchytInstanceID = 0;
viewedObjectReference = null;
if (viewedObject == null || viewedObject.Equals(null))
return;
bool fromHierarchy = viewedObject.scene.IsValid();
if (fromHierarchy)
viewedObjecHierarchytInstanceID = viewedObject.GetInstanceID();
else
viewedObjectAssetGUID = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(viewedObject));
viewedObjectReference = viewedObject;
}
//WARNING: only for script reloading
void LoadViewedObject()
{
viewedObjectReference = null;
GUID storedGUID;
GUID.TryParse(viewedObjectAssetGUID, out storedGUID);
if (!storedGUID.Empty())
{
string path = AssetDatabase.GUIDToAssetPath(viewedObjectAssetGUID);
viewedObjectReference = AssetDatabase.LoadAssetAtPath<GameObject>(path);
}
else if (viewedObjecHierarchytInstanceID != 0)
{
viewedObjectReference = EditorUtility.InstanceIDToObject(viewedObjecHierarchytInstanceID) as GameObject;
}
}
internal void LoadAll(bool reloadWithTemporaryID)
{
if (!reloadWithTemporaryID)
CleanTemporaryObjectIndexes();
LoadEnvironmentFromGUID();
LoadViewedObject();
}
internal void CleanTemporaryObjectIndexes()
=> viewedObjecHierarchytInstanceID = 0;
/// <summary>Reset the camera state to default values</summary>
public void ResetCameraState()
=> camera.Reset();
}
/// <summary>
/// Class that will contain debug value used.
/// </summary>
public class DebugContext
{
/// <summary>Display shadows in view.</summary>
public bool shadow = true;
/// <summary>Debug mode displayed. -1 means none.</summary>
public int viewMode = -1;
///// <summary>Display the debug grey balls</summary>
//public bool greyBalls;
//[SerializeField]
//string colorChartGUID = ""; //Empty GUID
///// <summary>The currently used color chart</summary>
//public Texture2D colorChart { get; private set; }
}
}

View File

@@ -0,0 +1,133 @@
Shader "Hidden/LookDev/CubeToLatlong"
{
Properties
{
[NoScaleOffset] _MainTex ("Cubemap", Any) = "grey" {}
_CubeToLatLongParams ("Parameters", Vector) = (0.0, 0.0, 0.0, 0.0)
_WindowParams("Window params", Vector) = (0.0, 0.0, 0.0, 0.0)
}
CGINCLUDE
#include "UnityCG.cginc"
uniform float4 _MainTex_HDR;
uniform float4 _MainTex_ST;
UNITY_DECLARE_TEXCUBE(_MainTex);
uniform float4 _CubeToLatLongParams; // x angle offset, y alpha, z intensity w lod to use
uniform float4 _WindowParams; // x Editor windows height, y Environment windows posY, z margin (constant of 2), w PixelsPerPoint
uniform bool _ManualTex2SRGB;
#define OutputAlpha _CubeToLatLongParams.y
#define Intensity _CubeToLatLongParams.z
#define CurrentLOD _CubeToLatLongParams.w
struct appdata_t
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
};
struct v2f
{
float2 texcoord : TEXCOORD0;
float4 vertex : SV_POSITION;
};
v2f vert(appdata_t IN)
{
v2f OUT;
OUT.vertex = UnityObjectToClipPos(IN.vertex);
OUT.texcoord = TRANSFORM_TEX(IN.texcoord, _MainTex);
return OUT;
}
float4 frag( float2 texcoord : TEXCOORD0,
UNITY_VPOS_TYPE vpos : VPOS
) : SV_Target
{
float2 texCoord = texcoord.xy;
float theta = texCoord.y * UNITY_PI;
float phi = (texCoord.x * 2.f * UNITY_PI - UNITY_PI*0.5f) - _CubeToLatLongParams.x;
float cosTheta = cos(theta);
float sinTheta = sqrt(1.0f - min(1.0f, cosTheta*cosTheta));
float cosPhi = cos(phi);
float sinPhi = sin(phi);
float3 direction = float3(sinTheta*cosPhi, cosTheta, sinTheta*sinPhi);
direction.xy *= -1.0;
float4 ret = float4(DecodeHDR(UNITY_SAMPLE_TEXCUBE_LOD(_MainTex, direction, CurrentLOD), _MainTex_HDR) * Intensity, OutputAlpha);
if (_ManualTex2SRGB)
ret.rgb = LinearToGammaSpace(ret.rgb);
// Clip outside of the library window
// Editor windows is like this:
//------
// Margin (2)
// Scene - Game - Asset Store <= What we call tab size
//------
// Settings - Views <= what we call menu size
//----
// View size with Environment windows)
//
// _WindowParams.x contain the height of the editor windows
// _WindowParams.y contain the start of the windows environment in the windows editor, i.e the menu size + tab size
// _WindowParams.z contain a constant margin of 2 (don't know how to retrieve that)
// _WindowParams.w is PixelsPerPoin (To handle retina display on OSX))
// We use VPOS register to clip, VPOS is dependent on the API. It is reversed in openGL.
// There is no need to clip when y is above height because the editor windows will clip it
// vertex.y is relative to editor windows
#if UNITY_UV_STARTS_AT_TOP
if ((vpos.y / _WindowParams.w) < (_WindowParams.y + _WindowParams.z))
#else
// vertex.y is reversed (start from bottom of the editor windsows)
vpos.y = _WindowParams.x - (vpos.y / _WindowParams.w);
if (vpos.y < _WindowParams.z)
#endif
{
clip(-1);
}
return ret;
}
ENDCG
SubShader
{
Tags
{
"ForceSupported"="True"
}
Lighting Off
Cull Off
ZTest Always
ZWrite Off
Pass
{
Blend One Zero
CGPROGRAM
#pragma fragment frag
#pragma vertex vert
#pragma target 3.0
ENDCG
}
Pass
{
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma fragment frag
#pragma vertex vert
#pragma target 3.0
ENDCG
}
}
}

View File

@@ -0,0 +1,69 @@
/* ******************************* */
/* override for personal skin them */
/* ******************************* */
#environmentContainer > EnvironmentElement
{
border-color: #999999;
}
#separator
{
border-color: #999999;
}
.list-environment-overlay > ToolbarButton
{
background-color: #CBCBCB;
}
.list-environment-overlay > ToolbarButton:hover
{
background-color: #e8e8e8;
}
#inspector-header
{
background-color: #CBCBCB;
border-color: #999999;
}
#separator-line
{
background-color: #CBCBCB;
}
Image.unity-list-view__item:selected
{
border-color: #3A72B0;
}
#environmentContainer
{
border-color: #999999;
}
#debugContainer
{
border-color: #999999;
}
#debugToolbar
{
border-color: #999999;
}
MultipleSourcePopupField > MultipleDifferentValue
{
background-color: #DFDFDF;
}
MultipleSourcePopupField > MultipleDifferentValue:hover
{
background-color: #e8e8e8;
}
#sunToBrightestButton:hover
{
background-color: #e8e8e8;
}

View File

@@ -0,0 +1,318 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace UnityEditor.Rendering.LookDev
{
partial class DisplayWindow
{
static partial class Style
{
internal const string k_DebugViewLabel = "Selected View";
internal const string k_DebugShadowLabel = "Display Shadows";
internal const string k_DebugViewMode = "View Mode";
internal static readonly Texture2D k_LockOpen = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Unlocked", forceLowRes: true);
internal static readonly Texture2D k_LockClose = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Locked", forceLowRes: true);
// /!\ WARNING:
//The following const are used in the uss.
//If you change them, update the uss file too.
internal const string k_DebugToolbarName = "debugToolbar";
}
MultipleSourceToggle m_Shadow;
MultipleSourcePopupField m_DebugView;
List<string> listDebugMode;
bool cameraSynced
=> LookDev.currentContext.cameraSynced;
ViewContext lastFocusedViewContext
=> LookDev.currentContext.GetViewContent(LookDev.currentContext.layout.lastFocusedView);
TargetDebugView targetDebugView
{
get => layout.debugPanelSource;
set
{
if (layout.debugPanelSource != value)
{
layout.debugPanelSource = value;
UpdateSideDebugPanelProperties();
}
}
}
bool debugView1SidePanel
=> targetDebugView == TargetDebugView.First
|| targetDebugView == TargetDebugView.Both;
bool debugView2SidePanel
=> targetDebugView == TargetDebugView.Second
|| targetDebugView == TargetDebugView.Both;
void ApplyInFilteredViewsContext(Action<ViewContext> action)
{
if (debugView1SidePanel)
action.Invoke(LookDev.currentContext.GetViewContent(ViewIndex.First));
if (debugView2SidePanel)
action.Invoke(LookDev.currentContext.GetViewContent(ViewIndex.Second));
}
T GetInFilteredViewsContext<T>(Func<ViewContext, T> getter, out bool multipleDifferentValue)
{
T res1 = default;
T res2 = default;
multipleDifferentValue = false;
bool view1 = debugView1SidePanel;
bool view2 = debugView2SidePanel;
if (view1)
res1 = getter.Invoke(LookDev.currentContext.GetViewContent(ViewIndex.First));
if (view2)
res2 = getter.Invoke(LookDev.currentContext.GetViewContent(ViewIndex.Second));
if (view1 && view2 && !res1.Equals(res2))
{
multipleDifferentValue = true;
return default;
}
else
return view1 ? res1 : res2;
}
void ReadValueFromSourcesWithoutNotify<T, K>(K element, Func<ViewContext, T> from)
where K : BaseField<T>, IMultipleSource
{
bool multipleDifferentValue;
T newValue = GetInFilteredViewsContext(from, out multipleDifferentValue);
if (element is MultipleSourcePopupField)
element.inMultipleValueState = multipleDifferentValue;
element.SetValueWithoutNotify(newValue);
element.inMultipleValueState = multipleDifferentValue;
}
#region Hack_Support_UIElement_MixedValueState
class MultipleDifferentValue : TextElement
{
public new class UxmlFactory : UxmlFactory<MultipleDifferentValue, UxmlTraits> {}
public new class UxmlTraits : TextElement.UxmlTraits {}
public new static readonly string ussClassName = "unity-multipledifferentevalue";
public MultipleDifferentValue()
{
AddToClassList(ussClassName);
text = "-";
}
}
interface IMultipleSource
{
bool inMultipleValueState { get; set; }
}
class MultipleSourceToggle : Toggle, IMultipleSource
{
MultipleDifferentValue m_MultipleOverlay;
bool m_InMultipleValueState = false;
public bool inMultipleValueState
{
get => m_InMultipleValueState;
set
{
if (value != m_InMultipleValueState)
{
m_MultipleOverlay.style.display = value ? DisplayStyle.Flex : DisplayStyle.None;
m_InMultipleValueState = value;
}
}
}
public MultipleSourceToggle() : base()
{
m_MultipleOverlay = new MultipleDifferentValue();
this.Q(name: "unity-checkmark").Add(m_MultipleOverlay);
m_MultipleOverlay.style.display = DisplayStyle.None;
}
public MultipleSourceToggle(string label) : base(label)
{
m_MultipleOverlay = new MultipleDifferentValue();
this.Q(name: "unity-checkmark").Add(m_MultipleOverlay);
m_MultipleOverlay.style.display = DisplayStyle.None;
}
public override void SetValueWithoutNotify(bool newValue)
{
if (inMultipleValueState)
inMultipleValueState = false;
base.SetValueWithoutNotify(newValue);
}
public override bool value
{
get => inMultipleValueState ? default : base.value;
set
{
if (inMultipleValueState)
inMultipleValueState = false;
base.value = value;
}
}
}
class MultipleSourcePopupField : PopupField<string>, IMultipleSource
{
internal readonly int count;
MultipleDifferentValue m_MultipleOverlay;
bool m_InMultipleValueState = false;
public bool inMultipleValueState
{
get => m_InMultipleValueState;
set
{
if (value != m_InMultipleValueState)
{
m_MultipleOverlay.style.display = value ? DisplayStyle.Flex : DisplayStyle.None;
m_InMultipleValueState = value;
}
}
}
public MultipleSourcePopupField(string label, List<string> choices, int defaultIndex = 0)
: base(
label,
choices,
defaultIndex,
null,
null)
{
count = choices.Count;
m_MultipleOverlay = new MultipleDifferentValue();
Add(m_MultipleOverlay);
m_MultipleOverlay.style.display = DisplayStyle.None;
}
public override void SetValueWithoutNotify(string newValue)
{
// forbid change from not direct selection as it can be find different value at opening
if (!inMultipleValueState)
base.SetValueWithoutNotify(newValue);
}
public override string value
{
get => inMultipleValueState ? default : base.value;
set
{
//when actively changing in the drop down, quit mixed value state
if (inMultipleValueState)
inMultipleValueState = false;
base.value = value;
}
}
}
#endregion
void CreateDebug()
{
if (m_MainContainer == null || m_MainContainer.Equals(null))
throw new System.MemberAccessException("m_MainContainer should be assigned prior CreateEnvironment()");
m_DebugContainer = new VisualElement() { name = Style.k_DebugContainerName };
m_MainContainer.Add(m_DebugContainer);
if (sidePanel == SidePanel.Debug)
m_MainContainer.AddToClassList(Style.k_ShowDebugPanelClass);
AddDebugViewSelector();
AddDebugShadow();
AddDebugViewMode();
//[TODO: finish]
//Toggle greyBalls = new Toggle("Grey balls");
//greyBalls.SetValueWithoutNotify(LookDev.currentContext.GetViewContent(LookDev.currentContext.layout.lastFocusedView).debug.greyBalls);
//greyBalls.RegisterValueChangedCallback(evt =>
//{
// LookDev.currentContext.GetViewContent(LookDev.currentContext.layout.lastFocusedView).debug.greyBalls = evt.newValue;
//});
//m_DebugContainer.Add(greyBalls);
//[TODO: debug why list sometimes empty on resource reloading]
//[TODO: display only per view]
if (sidePanel == SidePanel.Debug)
UpdateSideDebugPanelProperties();
}
void AddDebugViewSelector()
{
ToolbarRadio viewSelector = new ToolbarRadio()
{
name = Style.k_DebugToolbarName
};
viewSelector.AddRadios(new[]
{
Style.k_Camera1Icon,
Style.k_LinkIcon,
Style.k_Camera2Icon
});
viewSelector.SetValueWithoutNotify((int)targetDebugView);
viewSelector.RegisterValueChangedCallback(evt
=> targetDebugView = (TargetDebugView)evt.newValue);
m_DebugContainer.Add(viewSelector);
}
void AddDebugShadow()
{
m_Shadow = new MultipleSourceToggle(Style.k_DebugShadowLabel);
ReadValueFromSourcesWithoutNotify(m_Shadow, view => view.debug.shadow);
m_Shadow.RegisterValueChangedCallback(evt
=> ApplyInFilteredViewsContext(view => view.debug.shadow = evt.newValue));
m_DebugContainer.Add(m_Shadow);
}
void AddDebugViewMode(int pos = -1)
{
//if debugPanel is open on script reload, at this time
//RenderPipelineManager.currentPipeline is still null.
//So compute the list on next frame only.
if (LookDev.dataProvider == null)
{
EditorApplication.delayCall += () => AddDebugViewMode(2); //2 = hardcoded position of this field
return;
}
if (m_DebugView != null && m_DebugContainer.Contains(m_DebugView))
m_DebugContainer.Remove(m_DebugView);
listDebugMode = new List<string>(LookDev.dataProvider?.supportedDebugModes ?? Enumerable.Empty<string>());
listDebugMode.Insert(0, "None");
m_DebugView = new MultipleSourcePopupField(Style.k_DebugViewMode, listDebugMode);
if (sidePanel == SidePanel.Debug)
ReadValueFromSourcesWithoutNotify(m_DebugView, view => listDebugMode[view.debug.viewMode + 1]);
m_DebugView.RegisterValueChangedCallback(evt
=> ApplyInFilteredViewsContext(view => view.debug.viewMode = listDebugMode.IndexOf(evt.newValue) - 1));
if (pos == -1)
m_DebugContainer.Add(m_DebugView);
else
m_DebugContainer.Insert(pos, m_DebugView);
}
void UpdateSideDebugPanelProperties()
{
ReadValueFromSourcesWithoutNotify(m_Shadow, view => view.debug.shadow);
if (m_DebugView != null)
ReadValueFromSourcesWithoutNotify(m_DebugView, view => listDebugMode[view.debug.viewMode + 1]);
}
}
}

View File

@@ -0,0 +1,482 @@
using System;
using System.Collections.Generic;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace UnityEditor.Rendering.LookDev
{
/// <summary>Interface that must implement the EnvironmentLibrary view to communicate with the data management</summary>
public interface IEnvironmentDisplayer
{
/// <summary>Repaint the UI</summary>
void Repaint();
/// <summary>Callback on Environment change in the Library</summary>
event Action<EnvironmentLibrary> OnChangingEnvironmentLibrary;
}
partial class DisplayWindow : IEnvironmentDisplayer
{
static partial class Style
{
internal static readonly Texture2D k_AddIcon = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Add", forceLowRes: true);
internal static readonly Texture2D k_RemoveIcon = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Remove", forceLowRes: true);
internal static readonly Texture2D k_DuplicateIcon = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Duplicate", forceLowRes: true);
internal const string k_DragAndDropLibrary = "Drag and drop EnvironmentLibrary here";
}
VisualElement m_EnvironmentContainer;
ListView m_EnvironmentList;
EnvironmentElement m_EnvironmentInspector;
UIElements.Toolbar m_EnvironmentListToolbar;
UIElements.ObjectField m_LibraryField;
//event Action<UnityEngine.Object> OnAddingEnvironmentInternal;
//event Action<UnityEngine.Object> IEnvironmentDisplayer.OnAddingEnvironment
//{
// add => OnAddingEnvironmentInternal += value;
// remove => OnAddingEnvironmentInternal -= value;
//}
//event Action<int> OnRemovingEnvironmentInternal;
//event Action<int> IEnvironmentDisplayer.OnRemovingEnvironment
//{
// add => OnRemovingEnvironmentInternal += value;
// remove => OnRemovingEnvironmentInternal -= value;
//}
event Action<EnvironmentLibrary> OnChangingEnvironmentLibraryInternal;
event Action<EnvironmentLibrary> IEnvironmentDisplayer.OnChangingEnvironmentLibrary
{
add => OnChangingEnvironmentLibraryInternal += value;
remove => OnChangingEnvironmentLibraryInternal -= value;
}
static int FirstVisibleIndex(ListView listView)
=> (int)(listView.Q<ScrollView>().scrollOffset.y / listView.itemHeight);
void CreateEnvironment()
{
if (m_MainContainer == null || m_MainContainer.Equals(null))
throw new System.MemberAccessException("m_MainContainer should be assigned prior CreateEnvironment()");
m_EnvironmentContainer = new VisualElement() { name = Style.k_EnvironmentContainerName };
m_MainContainer.Add(m_EnvironmentContainer);
if (sidePanel == SidePanel.Environment)
m_MainContainer.AddToClassList(Style.k_ShowEnvironmentPanelClass);
m_EnvironmentInspector = new EnvironmentElement(withPreview: false, () =>
{
LookDev.SaveContextChangeAndApply(ViewIndex.First);
LookDev.SaveContextChangeAndApply(ViewIndex.Second);
});
m_EnvironmentList = new ListView();
m_EnvironmentList.AddToClassList("list-environment");
m_EnvironmentList.selectionType = SelectionType.Single;
m_EnvironmentList.itemHeight = EnvironmentElement.k_SkyThumbnailHeight;
m_EnvironmentList.makeItem = () =>
{
var preview = new Image();
preview.AddManipulator(new EnvironmentPreviewDragger(this, m_ViewContainer));
return preview;
};
m_EnvironmentList.bindItem = (e, i) =>
{
if (LookDev.currentContext.environmentLibrary == null)
return;
(e as Image).image = EnvironmentElement.GetLatLongThumbnailTexture(
LookDev.currentContext.environmentLibrary[i],
EnvironmentElement.k_SkyThumbnailWidth);
};
#if UNITY_2020_1_OR_NEWER
m_EnvironmentList.onSelectionChange += objects =>
{
bool empty = !objects.GetEnumerator().MoveNext();
if (empty || (LookDev.currentContext.environmentLibrary?.Count ?? 0) == 0)
#else
m_EnvironmentList.onSelectionChanged += objects =>
{
if (objects.Count == 0 || (LookDev.currentContext.environmentLibrary?.Count ?? 0) == 0)
#endif
{
m_EnvironmentInspector.style.visibility = Visibility.Hidden;
m_EnvironmentInspector.style.height = 0;
}
else
{
m_EnvironmentInspector.style.visibility = Visibility.Visible;
m_EnvironmentInspector.style.height = new StyleLength(StyleKeyword.Auto);
int firstVisibleIndex = FirstVisibleIndex(m_EnvironmentList);
Environment environment = LookDev.currentContext.environmentLibrary[m_EnvironmentList.selectedIndex];
var container = m_EnvironmentList.Q("unity-content-container");
if (m_EnvironmentList.selectedIndex - firstVisibleIndex >= container.childCount || m_EnvironmentList.selectedIndex < firstVisibleIndex)
{
m_EnvironmentList.ScrollToItem(m_EnvironmentList.selectedIndex);
firstVisibleIndex = FirstVisibleIndex(m_EnvironmentList);
}
Image deportedLatLong = container[m_EnvironmentList.selectedIndex - firstVisibleIndex] as Image;
m_EnvironmentInspector.Bind(environment, deportedLatLong);
}
};
#if UNITY_2020_1_OR_NEWER
m_EnvironmentList.onItemsChosen += objCollection =>
{
foreach (var obj in objCollection)
EditorGUIUtility.PingObject(LookDev.currentContext.environmentLibrary ? [(int)obj]);
};
#else
m_EnvironmentList.onItemChosen += obj =>
EditorGUIUtility.PingObject(LookDev.currentContext.environmentLibrary ? [(int)obj]);
#endif
m_NoEnvironmentList = new Label(Style.k_DragAndDropLibrary);
m_NoEnvironmentList.style.flexGrow = 1;
m_NoEnvironmentList.style.unityTextAlign = TextAnchor.MiddleCenter;
m_EnvironmentContainer.Add(m_EnvironmentInspector);
m_EnvironmentListToolbar = new UIElements.Toolbar();
ToolbarButton addEnvironment = new ToolbarButton(() =>
{
if (LookDev.currentContext.environmentLibrary == null)
return;
LookDev.currentContext.environmentLibrary.Add();
RefreshLibraryDisplay();
m_EnvironmentList.ScrollToItem(-1); //-1: scroll to end
m_EnvironmentList.selectedIndex = LookDev.currentContext.environmentLibrary.Count - 1;
ScrollToEnd();
})
{
name = "add",
tooltip = "Add new empty environment"
};
addEnvironment.Add(new Image() { image = Style.k_AddIcon });
ToolbarButton removeEnvironment = new ToolbarButton(() =>
{
if (m_EnvironmentList.selectedIndex == -1 || LookDev.currentContext.environmentLibrary == null)
return;
LookDev.currentContext.environmentLibrary?.Remove(m_EnvironmentList.selectedIndex);
RefreshLibraryDisplay();
m_EnvironmentList.selectedIndex = -1;
})
{
name = "remove",
tooltip = "Remove environment currently selected"
};
removeEnvironment.Add(new Image() { image = Style.k_RemoveIcon });
ToolbarButton duplicateEnvironment = new ToolbarButton(() =>
{
if (m_EnvironmentList.selectedIndex == -1 || LookDev.currentContext.environmentLibrary == null)
return;
LookDev.currentContext.environmentLibrary.Duplicate(m_EnvironmentList.selectedIndex);
RefreshLibraryDisplay();
m_EnvironmentList.ScrollToItem(-1); //-1: scroll to end
m_EnvironmentList.selectedIndex = LookDev.currentContext.environmentLibrary.Count - 1;
ScrollToEnd();
})
{
name = "duplicate",
tooltip = "Duplicate environment currently selected"
};
duplicateEnvironment.Add(new Image() { image = Style.k_DuplicateIcon });
m_EnvironmentListToolbar.Add(addEnvironment);
m_EnvironmentListToolbar.Add(removeEnvironment);
m_EnvironmentListToolbar.Add(duplicateEnvironment);
m_EnvironmentListToolbar.AddToClassList("list-environment-overlay");
var m_EnvironmentInspectorSeparator = new VisualElement() { name = "separator-line" };
m_EnvironmentInspectorSeparator.Add(new VisualElement() { name = "separator" });
m_EnvironmentContainer.Add(m_EnvironmentInspectorSeparator);
VisualElement listContainer = new VisualElement();
listContainer.AddToClassList("list-environment");
listContainer.Add(m_EnvironmentList);
listContainer.Add(m_EnvironmentListToolbar);
m_LibraryField = new ObjectField("Library")
{
tooltip = "The currently used library"
};
m_LibraryField.allowSceneObjects = false;
m_LibraryField.objectType = typeof(EnvironmentLibrary);
m_LibraryField.SetValueWithoutNotify(LookDev.currentContext.environmentLibrary);
m_LibraryField.RegisterValueChangedCallback(evt =>
{
m_EnvironmentList.selectedIndex = -1;
OnChangingEnvironmentLibraryInternal?.Invoke(evt.newValue as EnvironmentLibrary);
RefreshLibraryDisplay();
});
var environmentListCreationToolbar = new UIElements.Toolbar()
{
name = "environmentListCreationToolbar"
};
environmentListCreationToolbar.Add(m_LibraryField);
environmentListCreationToolbar.Add(new ToolbarButton(()
=> EnvironmentLibraryCreator.CreateAndAssignTo(m_LibraryField))
{
text = "New",
tooltip = "Create a new EnvironmentLibrary"
});
m_EnvironmentContainer.Add(listContainer);
m_EnvironmentContainer.Add(m_NoEnvironmentList);
m_EnvironmentContainer.Add(environmentListCreationToolbar);
//add ability to unselect
m_EnvironmentList.RegisterCallback<MouseDownEvent>(evt =>
{
var clickedIndex = (int)(evt.localMousePosition.y / m_EnvironmentList.itemHeight);
if (clickedIndex >= m_EnvironmentList.itemsSource.Count)
{
m_EnvironmentList.selectedIndex = -1;
evt.StopPropagation();
}
});
RefreshLibraryDisplay();
}
//necessary as the scrollview need to be updated which take some editor frames.
void ScrollToEnd(int attemptRemaining = 5)
{
m_EnvironmentList.ScrollToItem(-1); //-1: scroll to end
if (attemptRemaining > 0)
EditorApplication.delayCall += () => ScrollToEnd(--attemptRemaining);
}
void RefreshLibraryDisplay()
{
if (m_LibraryField != null)
m_LibraryField.SetValueWithoutNotify(LookDev.currentContext.environmentLibrary);
if (m_EnvironmentInspector != null && m_EnvironmentList != null)
{
int itemMax = LookDev.currentContext.environmentLibrary?.Count ?? 0;
if (itemMax == 0 || m_EnvironmentList.selectedIndex == -1)
{
m_EnvironmentInspector.style.visibility = Visibility.Hidden;
m_EnvironmentInspector.style.height = 0;
}
else
{
m_EnvironmentInspector.style.visibility = Visibility.Visible;
m_EnvironmentInspector.style.height = new StyleLength(StyleKeyword.Auto);
}
var items = new List<int>(itemMax);
for (int i = 0; i < itemMax; i++)
items.Add(i);
m_EnvironmentList.itemsSource = items;
if (LookDev.currentContext.environmentLibrary == null)
{
m_EnvironmentList
.Q(className: "unity-scroll-view__vertical-scroller")
.Q("unity-dragger")
.style.visibility = Visibility.Hidden;
m_EnvironmentListToolbar.style.visibility = Visibility.Hidden;
m_NoEnvironmentList.style.display = DisplayStyle.Flex;
}
else
{
m_EnvironmentList
.Q(className: "unity-scroll-view__vertical-scroller")
.Q("unity-dragger")
.style.visibility = itemMax == 0
? Visibility.Hidden
: Visibility.Visible;
m_EnvironmentListToolbar.style.visibility = Visibility.Visible;
m_NoEnvironmentList.style.display = DisplayStyle.None;
}
}
}
DraggingContext StartDragging(VisualElement item, Vector2 worldPosition)
=> new DraggingContext(
rootVisualElement,
item as Image,
//note: this even can come before the selection event of the
//ListView. Reconstruct index by looking at target of the event.
(int)item.layout.y / m_EnvironmentList.itemHeight,
worldPosition);
void EndDragging(DraggingContext context, Vector2 mouseWorldPosition)
{
Environment environment = LookDev.currentContext.environmentLibrary ? [context.draggedIndex];
if (environment == null)
return;
if (m_Views[(int)ViewIndex.First].ContainsPoint(mouseWorldPosition))
{
if (viewLayout == Layout.CustomSplit)
OnChangingEnvironmentInViewInternal?.Invoke(environment, ViewCompositionIndex.Composite, mouseWorldPosition);
else
OnChangingEnvironmentInViewInternal?.Invoke(environment, ViewCompositionIndex.First, mouseWorldPosition);
m_NoEnvironment1.style.visibility = environment == null || environment.Equals(null) ? Visibility.Visible : Visibility.Hidden;
}
else
{
OnChangingEnvironmentInViewInternal?.Invoke(environment, ViewCompositionIndex.Second, mouseWorldPosition);
m_NoEnvironment2.style.visibility = environment == null || environment.Equals(null) ? Visibility.Visible : Visibility.Hidden;
}
}
class DraggingContext : IDisposable
{
const string k_CursorFollowerName = "cursorFollower";
public readonly int draggedIndex;
readonly Image cursorFollower;
readonly Vector2 cursorOffset;
readonly VisualElement windowContent;
public DraggingContext(VisualElement windowContent, Image draggedElement, int draggedIndex, Vector2 worldPosition)
{
this.windowContent = windowContent;
this.draggedIndex = draggedIndex;
cursorFollower = new Image()
{
name = k_CursorFollowerName,
image = draggedElement.image
};
cursorFollower.tintColor = new Color(1f, 1f, 1f, .5f);
windowContent.Add(cursorFollower);
cursorOffset = draggedElement.WorldToLocal(worldPosition);
cursorFollower.style.position = Position.Absolute;
UpdateCursorFollower(worldPosition);
}
public void UpdateCursorFollower(Vector2 mouseWorldPosition)
{
Vector2 windowLocalPosition = windowContent.WorldToLocal(mouseWorldPosition);
cursorFollower.style.left = windowLocalPosition.x - cursorOffset.x;
cursorFollower.style.top = windowLocalPosition.y - cursorOffset.y;
}
public void Dispose()
{
if (windowContent.Contains(cursorFollower))
windowContent.Remove(cursorFollower);
}
}
class EnvironmentPreviewDragger : Manipulator
{
VisualElement m_DropArea;
DisplayWindow m_Window;
//Note: static as only one drag'n'drop at a time
static DraggingContext s_Context;
public EnvironmentPreviewDragger(DisplayWindow window, VisualElement dropArea)
{
m_Window = window;
m_DropArea = dropArea;
}
protected override void RegisterCallbacksOnTarget()
{
target.RegisterCallback<MouseDownEvent>(OnMouseDown);
target.RegisterCallback<MouseUpEvent>(OnMouseUp);
}
protected override void UnregisterCallbacksFromTarget()
{
target.UnregisterCallback<MouseDownEvent>(OnMouseDown);
target.UnregisterCallback<MouseUpEvent>(OnMouseUp);
}
void Release()
{
target.UnregisterCallback<MouseMoveEvent>(OnMouseMove);
s_Context?.Dispose();
target.ReleaseMouse();
s_Context = null;
}
void OnMouseDown(MouseDownEvent evt)
{
if (evt.button == 0)
{
target.CaptureMouse();
target.RegisterCallback<MouseMoveEvent>(OnMouseMove);
s_Context = m_Window.StartDragging(target, evt.mousePosition);
//do not stop event as we still need to propagate it to the ListView for selection
}
}
void OnMouseUp(MouseUpEvent evt)
{
if (evt.button != 0)
return;
if (m_DropArea.ContainsPoint(m_DropArea.WorldToLocal(Event.current.mousePosition)))
{
m_Window.EndDragging(s_Context, evt.mousePosition);
evt.StopPropagation();
}
Release();
}
void OnMouseMove(MouseMoveEvent evt)
{
evt.StopPropagation();
s_Context.UpdateCursorFollower(evt.mousePosition);
}
}
void IEnvironmentDisplayer.Repaint()
{
//can be unsync if library asset is destroy by user, so if null force sync
if (LookDev.currentContext.environmentLibrary == null)
m_LibraryField.value = null;
RefreshLibraryDisplay();
}
void OnFocus()
{
//OnFocus is called before OnEnable that open backend if not already opened, so only sync if backend is open
if (LookDev.open)
{
//If EnvironmentLibrary asset as been edited by the user (deletion),
//update all view to use null environment if it was not temporary ones
if (LookDev.currentContext.HasLibraryAssetChanged(m_LibraryField.value as EnvironmentLibrary))
{
ViewContext viewContext = LookDev.currentContext.GetViewContent(ViewIndex.First);
if (!(viewContext.environment?.isCubemapOnly ?? false))
OnChangingEnvironmentInViewInternal?.Invoke(viewContext.environment, ViewCompositionIndex.First, default);
viewContext = LookDev.currentContext.GetViewContent(ViewIndex.Second);
if (!(viewContext.environment?.isCubemapOnly ?? false))
OnChangingEnvironmentInViewInternal?.Invoke(viewContext.environment, ViewCompositionIndex.Second, default);
}
//If Cubemap asset as been edited by the user (deletion),
//update all views to use null environment if it was temporary ones
//and update all other views' environment to not use cubemap anymore
foreach (ViewContext viewContext in LookDev.currentContext.viewContexts)
{
if (viewContext.environment == null || !viewContext.environment.HasCubemapAssetChanged(viewContext.environment.cubemap))
continue;
if (viewContext.environment.isCubemapOnly)
viewContext.UpdateEnvironment(null);
else
viewContext.environment.cubemap = null;
}
((IEnvironmentDisplayer)this).Repaint();
}
}
void FullRefreshEnvironmentList()
{
if (LookDev.currentContext.environmentLibrary != null)
LookDev.currentContext.FullReimportEnvironmentLibrary();
((IEnvironmentDisplayer)this).Repaint();
}
}
}

View File

@@ -0,0 +1,689 @@
using System;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.UIElements;
using RenderPipelineManager = UnityEngine.Rendering.RenderPipelineManager;
namespace UnityEditor.Rendering.LookDev
{
/// <summary>Interface that must implement the viewer to communicate with the compositor and data management</summary>
public interface IViewDisplayer
{
/// <summary>Get the displayed rect to use</summary>
/// <param name="index">Index of this view</param>
/// <returns>The Rect to draw</returns>
Rect GetRect(ViewCompositionIndex index);
/// <summary>Set the computed texture in the view</summary>
/// <param name="index">Index of this view</param>
/// <param name="texture">The texture used</param>
void SetTexture(ViewCompositionIndex index, Texture texture);
/// <summary>Repaint the UI</summary>
void Repaint();
/// <summary>Callback on layout changed</summary>
event Action<Layout, SidePanel> OnLayoutChanged;
/// <summary>Callback on RenderDoc acquisition is triggered</summary>
event Action OnRenderDocAcquisitionTriggered;
/// <summary>Callback on ;ouse events in the view</summary>
event Action<IMouseEvent> OnMouseEventInView;
/// <summary>Callback on object changed in the view</summary>
event Action<GameObject, ViewCompositionIndex, Vector2> OnChangingObjectInView;
/// <summary>Callback on environment changed in the view</summary>
event Action<UnityEngine.Object, ViewCompositionIndex, Vector2> OnChangingEnvironmentInView;
/// <summary>Callback on closed</summary>
event Action OnClosed;
/// <summary>Callback on update requested</summary>
event Action OnUpdateRequested;
}
partial class DisplayWindow : EditorWindow, IViewDisplayer
{
static partial class Style
{
internal const string k_IconFolder = @"Packages/com.unity.render-pipelines.core/Editor/LookDev/Icons/";
internal const string k_uss = @"Packages/com.unity.render-pipelines.core/Editor/LookDev/DisplayWindow.uss";
internal const string k_uss_personal_overload = @"Packages/com.unity.render-pipelines.core/Editor/LookDev/DisplayWindow-PersonalSkin.uss";
internal static readonly GUIContent k_WindowTitleAndIcon = EditorGUIUtility.TrTextContentWithIcon("Look Dev", CoreEditorUtils.LoadIcon(k_IconFolder, "LookDev", forceLowRes: true));
internal static readonly (Texture2D icon, string tooltip)k_Layout1Icon =
(CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Layout1", forceLowRes: true),
"First view");
internal static readonly (Texture2D icon, string tooltip)k_Layout2Icon =
(CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Layout2", forceLowRes: true),
"Second view");
internal static readonly (Texture2D icon, string tooltip)k_LayoutVerticalIcon =
(CoreEditorUtils.LoadIcon(Style.k_IconFolder, "LayoutVertical", forceLowRes: true),
"Both views split vertically");
internal static readonly (Texture2D icon, string tooltip)k_LayoutHorizontalIcon =
(CoreEditorUtils.LoadIcon(Style.k_IconFolder, "LayoutHorizontal", forceLowRes: true),
"Both views split horizontally");
internal static readonly (Texture2D icon, string tooltip)k_LayoutStackIcon =
(CoreEditorUtils.LoadIcon(Style.k_IconFolder, "LayoutCustom", forceLowRes: true),
"Both views stacked");
internal static readonly Texture2D k_Camera1Icon = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Camera1", forceLowRes: true);
internal static readonly Texture2D k_Camera2Icon = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Camera2", forceLowRes: true);
internal static readonly Texture2D k_LinkIcon = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Link", forceLowRes: true);
internal static readonly Texture2D k_RightIcon = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "RightArrow", forceLowRes: true);
internal static readonly Texture2D k_LeftIcon = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "LeftArrow", forceLowRes: true);
internal static readonly Texture2D k_RenderdocIcon = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "RenderDoc", forceLowRes: true);
internal const string k_RenderDocLabel = " Content";
internal const string k_CameraSyncTooltip = "Synchronize camera movement amongst views";
internal const string k_CameraMenuSync1On2 = "Align Camera 1 with Camera 2";
internal const string k_CameraMenuSync2On1 = "Align Camera 2 with Camera 1";
internal const string k_CameraMenuReset = "Reset Cameras";
internal const string k_EnvironmentSidePanelName = "Environment";
internal const string k_DebugSidePanelName = "Debug";
internal const string k_DragAndDropObject = "Drag and drop object here";
internal const string k_DragAndDropEnvironment = "Drag and drop environment from side panel here";
// /!\ WARNING:
//The following const are used in the uss.
//If you change them, update the uss file too.
internal const string k_MainContainerName = "mainContainer";
internal const string k_ViewContainerName = "viewContainer";
internal const string k_FirstViewName = "firstView";
internal const string k_SecondViewName = "secondView";
internal const string k_ToolbarName = "toolbar";
internal const string k_ToolbarRadioName = "toolbarRadio";
internal const string k_TabsRadioName = "tabsRadio";
internal const string k_SideToolbarName = "sideToolbar";
internal const string k_SharedContainerClass = "container";
internal const string k_FirstViewClass = "firstView";
internal const string k_SecondViewsClass = "secondView";
internal const string k_VerticalViewsClass = "verticalSplit";
internal const string k_DebugContainerName = "debugContainer";
internal const string k_ShowDebugPanelClass = "showDebugPanel";
internal const string k_EnvironmentContainerName = "environmentContainer";
internal const string k_ShowEnvironmentPanelClass = "showEnvironmentPanel";
internal const string k_CameraMenuName = "cameraMenu";
internal const string k_CameraButtonName = "cameraButton";
internal const string k_CameraSeparatorName = "cameraSeparator";
internal const string k_RenderDocContentName = "renderdoc-content";
}
VisualElement m_MainContainer;
VisualElement m_ViewContainer;
VisualElement m_DebugContainer;
Label m_NoEnvironmentList;
Label m_NoObject1;
Label m_NoEnvironment1;
Label m_NoObject2;
Label m_NoEnvironment2;
Image[] m_Views = new Image[2];
LayoutContext layout
=> LookDev.currentContext.layout;
Layout viewLayout
{
get => layout.viewLayout;
set
{
if (layout.viewLayout != value)
{
OnLayoutChangedInternal?.Invoke(value, sidePanel);
ApplyLayout(value);
}
}
}
SidePanel sidePanel
{
get => layout.showedSidePanel;
set
{
if (layout.showedSidePanel != value)
{
OnLayoutChangedInternal?.Invoke(viewLayout, value);
ApplySidePanelChange(layout.showedSidePanel);
}
}
}
event Action<Layout, SidePanel> OnLayoutChangedInternal;
event Action<Layout, SidePanel> IViewDisplayer.OnLayoutChanged
{
add => OnLayoutChangedInternal += value;
remove => OnLayoutChangedInternal -= value;
}
event Action OnRenderDocAcquisitionTriggeredInternal;
event Action IViewDisplayer.OnRenderDocAcquisitionTriggered
{
add => OnRenderDocAcquisitionTriggeredInternal += value;
remove => OnRenderDocAcquisitionTriggeredInternal -= value;
}
event Action<IMouseEvent> OnMouseEventInViewPortInternal;
event Action<IMouseEvent> IViewDisplayer.OnMouseEventInView
{
add => OnMouseEventInViewPortInternal += value;
remove => OnMouseEventInViewPortInternal -= value;
}
event Action<GameObject, ViewCompositionIndex, Vector2> OnChangingObjectInViewInternal;
event Action<GameObject, ViewCompositionIndex, Vector2> IViewDisplayer.OnChangingObjectInView
{
add => OnChangingObjectInViewInternal += value;
remove => OnChangingObjectInViewInternal -= value;
}
//event Action<Material, ViewCompositionIndex, Vector2> OnChangingMaterialInViewInternal;
//event Action<Material, ViewCompositionIndex, Vector2> IViewDisplayer.OnChangingMaterialInView
//{
// add => OnChangingMaterialInViewInternal += value;
// remove => OnChangingMaterialInViewInternal -= value;
//}
event Action<UnityEngine.Object, ViewCompositionIndex, Vector2> OnChangingEnvironmentInViewInternal;
event Action<UnityEngine.Object, ViewCompositionIndex, Vector2> IViewDisplayer.OnChangingEnvironmentInView
{
add => OnChangingEnvironmentInViewInternal += value;
remove => OnChangingEnvironmentInViewInternal -= value;
}
event Action OnClosedInternal;
event Action IViewDisplayer.OnClosed
{
add => OnClosedInternal += value;
remove => OnClosedInternal -= value;
}
event Action OnUpdateRequestedInternal;
event Action IViewDisplayer.OnUpdateRequested
{
add => OnUpdateRequestedInternal += value;
remove => OnUpdateRequestedInternal -= value;
}
StyleSheet styleSheet = null;
StyleSheet styleSheetLight = null;
SwitchableCameraController m_FirstOrCompositeManipulator;
CameraController m_SecondManipulator;
ComparisonGizmoController m_GizmoManipulator;
void ReloadStyleSheets()
{
if (styleSheet == null || styleSheet.Equals(null))
{
styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(Style.k_uss);
if (styleSheet == null || styleSheet.Equals(null))
{
//Debug.LogWarning("[LookDev] Could not load Stylesheet.");
return;
}
}
if (!rootVisualElement.styleSheets.Contains(styleSheet))
rootVisualElement.styleSheets.Add(styleSheet);
//Additively load Light Skin
if (!EditorGUIUtility.isProSkin)
{
if (styleSheetLight == null || styleSheetLight.Equals(null))
{
styleSheetLight = AssetDatabase.LoadAssetAtPath<StyleSheet>(Style.k_uss_personal_overload);
if (styleSheetLight == null || styleSheetLight.Equals(null))
{
//Debug.LogWarning("[LookDev] Could not load Light skin.");
return;
}
}
if (!rootVisualElement.styleSheets.Contains(styleSheetLight))
rootVisualElement.styleSheets.Add(styleSheetLight);
}
}
void CreateGUI()
{
ReloadStyleSheets();
//Call the open function to configure LookDev
// in case the window where open when last editor session finished.
// (Else it will open at start and has nothing to display).
if (!LookDev.open)
LookDev.Initialize(this);
titleContent = Style.k_WindowTitleAndIcon;
// /!\ be sure to have a minSize that will allow a non negative sized viewport even with side panel open
this.minSize = new Vector2(600, 400);
CreateToolbar();
m_MainContainer = new VisualElement() { name = Style.k_MainContainerName };
m_MainContainer.AddToClassList(Style.k_SharedContainerClass);
rootVisualElement.Add(m_MainContainer);
CreateViews();
CreateEnvironment();
CreateDebug();
CreateDropAreas();
ApplyLayout(viewLayout);
ApplySidePanelChange(layout.showedSidePanel);
}
void OnEnable()
{
Undo.undoRedoPerformed += FullRefreshEnvironmentList;
}
void OnDisable()
{
Undo.undoRedoPerformed -= FullRefreshEnvironmentList;
OnClosedInternal?.Invoke();
}
void CreateToolbar()
{
// Layout swapper part
var layoutRadio = new ToolbarRadio() { name = Style.k_ToolbarRadioName };
layoutRadio.AddRadios(new[]
{
Style.k_Layout1Icon,
Style.k_Layout2Icon,
Style.k_LayoutVerticalIcon,
Style.k_LayoutHorizontalIcon,
Style.k_LayoutStackIcon,
});
layoutRadio.RegisterCallback((ChangeEvent<int> evt)
=> viewLayout = (Layout)evt.newValue);
layoutRadio.SetValueWithoutNotify((int)viewLayout);
var cameraMenu = new ToolbarMenu() { name = Style.k_CameraMenuName };
cameraMenu.variant = ToolbarMenu.Variant.Popup;
var cameraToggle = new ToolbarToggle() { name = Style.k_CameraButtonName };
cameraToggle.value = LookDev.currentContext.cameraSynced;
cameraToggle.tooltip = Style.k_CameraSyncTooltip;
//Note: when having Image on top of the Toggle nested in the Menu, RegisterValueChangedCallback is not called
//cameraToggle.RegisterValueChangedCallback(evt => LookDev.currentContext.cameraSynced = evt.newValue);
cameraToggle.RegisterCallback<MouseUpEvent>(evt =>
{
LookDev.currentContext.cameraSynced ^= true;
cameraToggle.SetValueWithoutNotify(LookDev.currentContext.cameraSynced);
});
var cameraSeparator = new ToolbarToggle() { name = Style.k_CameraSeparatorName };
cameraToggle.Add(new Image() { image = Style.k_Camera1Icon });
cameraToggle.Add(new Image() { image = Style.k_LinkIcon });
cameraToggle.Add(new Image() { image = Style.k_Camera2Icon });
cameraMenu.Add(cameraSeparator);
cameraMenu.Add(cameraToggle);
cameraMenu.menu.AppendAction(Style.k_CameraMenuSync1On2,
(DropdownMenuAction a) => LookDev.currentContext.SynchronizeCameraStates(ViewIndex.Second),
DropdownMenuAction.AlwaysEnabled);
cameraMenu.menu.AppendAction(Style.k_CameraMenuSync2On1,
(DropdownMenuAction a) => LookDev.currentContext.SynchronizeCameraStates(ViewIndex.First),
DropdownMenuAction.AlwaysEnabled);
cameraMenu.menu.AppendAction(Style.k_CameraMenuReset,
(DropdownMenuAction a) =>
{
LookDev.currentContext.GetViewContent(ViewIndex.First).ResetCameraState();
LookDev.currentContext.GetViewContent(ViewIndex.Second).ResetCameraState();
},
DropdownMenuAction.AlwaysEnabled);
// Side part
var sideRadio = new ToolbarRadio(canDeselectAll: true)
{
name = Style.k_TabsRadioName
};
sideRadio.AddRadios(new[]
{
Style.k_EnvironmentSidePanelName,
Style.k_DebugSidePanelName,
});
sideRadio.SetValueWithoutNotify((int)sidePanel);
sideRadio.RegisterCallback((ChangeEvent<int> evt)
=> sidePanel = (SidePanel)evt.newValue);
// Aggregate parts
var toolbar = new UIElements.Toolbar() { name = Style.k_ToolbarName };
toolbar.Add(layoutRadio);
toolbar.Add(new ToolbarSpacer());
toolbar.Add(cameraMenu);
toolbar.Add(new ToolbarSpacer() { flex = true });
if (UnityEditorInternal.RenderDoc.IsInstalled() && UnityEditorInternal.RenderDoc.IsLoaded())
{
var renderDocButton = new ToolbarButton(() => OnRenderDocAcquisitionTriggeredInternal?.Invoke())
{
name = Style.k_RenderDocContentName
};
renderDocButton.Add(new Image() { image = Style.k_RenderdocIcon });
renderDocButton.Add(new Label() { text = Style.k_RenderDocLabel });
toolbar.Add(renderDocButton);
toolbar.Add(new ToolbarSpacer());
}
toolbar.Add(sideRadio);
rootVisualElement.Add(toolbar);
}
void CreateViews()
{
if (m_MainContainer == null || m_MainContainer.Equals(null))
throw new System.MemberAccessException("m_MainContainer should be assigned prior CreateViews()");
m_ViewContainer = new VisualElement() { name = Style.k_ViewContainerName };
m_ViewContainer.AddToClassList(LookDev.currentContext.layout.isMultiView ? Style.k_SecondViewsClass : Style.k_FirstViewClass);
m_ViewContainer.AddToClassList(Style.k_SharedContainerClass);
m_MainContainer.Add(m_ViewContainer);
m_ViewContainer.RegisterCallback<MouseDownEvent>(evt => OnMouseEventInViewPortInternal?.Invoke(evt));
m_ViewContainer.RegisterCallback<MouseUpEvent>(evt => OnMouseEventInViewPortInternal?.Invoke(evt));
m_ViewContainer.RegisterCallback<MouseMoveEvent>(evt => OnMouseEventInViewPortInternal?.Invoke(evt));
m_Views[(int)ViewIndex.First] = new Image() { name = Style.k_FirstViewName, image = Texture2D.blackTexture };
m_ViewContainer.Add(m_Views[(int)ViewIndex.First]);
m_Views[(int)ViewIndex.Second] = new Image() { name = Style.k_SecondViewName, image = Texture2D.blackTexture };
m_ViewContainer.Add(m_Views[(int)ViewIndex.Second]);
m_FirstOrCompositeManipulator = new SwitchableCameraController(
this,
index =>
{
LookDev.currentContext.SetFocusedCamera(index);
var environment = LookDev.currentContext.GetViewContent(index).environment;
if (sidePanel == SidePanel.Environment && environment != null && LookDev.currentContext.environmentLibrary != null)
m_EnvironmentList.selectedIndex = LookDev.currentContext.environmentLibrary.IndexOf(environment);
});
m_SecondManipulator = new CameraController(
this,
() =>
{
LookDev.currentContext.SetFocusedCamera(ViewIndex.Second);
var environment = LookDev.currentContext.GetViewContent(ViewIndex.Second).environment;
if (sidePanel == SidePanel.Environment && environment != null && LookDev.currentContext.environmentLibrary != null)
m_EnvironmentList.selectedIndex = LookDev.currentContext.environmentLibrary.IndexOf(environment);
});
m_GizmoManipulator = new ComparisonGizmoController(m_FirstOrCompositeManipulator);
m_Views[(int)ViewIndex.First].AddManipulator(m_GizmoManipulator); //must take event first to switch the firstOrCompositeManipulator
m_Views[(int)ViewIndex.First].AddManipulator(m_FirstOrCompositeManipulator);
m_Views[(int)ViewIndex.Second].AddManipulator(m_SecondManipulator);
m_NoObject1 = new Label(Style.k_DragAndDropObject);
m_NoObject1.style.flexGrow = 1;
m_NoObject1.style.unityTextAlign = TextAnchor.MiddleCenter;
m_NoObject2 = new Label(Style.k_DragAndDropObject);
m_NoObject2.style.flexGrow = 1;
m_NoObject2.style.unityTextAlign = TextAnchor.MiddleCenter;
m_NoEnvironment1 = new Label(Style.k_DragAndDropEnvironment);
m_NoEnvironment1.style.flexGrow = 1;
m_NoEnvironment1.style.unityTextAlign = TextAnchor.MiddleCenter;
m_NoEnvironment1.style.whiteSpace = WhiteSpace.Normal;
m_NoEnvironment2 = new Label(Style.k_DragAndDropEnvironment);
m_NoEnvironment2.style.flexGrow = 1;
m_NoEnvironment2.style.unityTextAlign = TextAnchor.MiddleCenter;
m_NoEnvironment2.style.whiteSpace = WhiteSpace.Normal;
m_Views[(int)ViewIndex.First].Add(m_NoObject1);
m_Views[(int)ViewIndex.First].Add(m_NoEnvironment1);
m_Views[(int)ViewIndex.Second].Add(m_NoObject2);
m_Views[(int)ViewIndex.Second].Add(m_NoEnvironment2);
}
void CreateDropAreas()
{
// GameObject or Prefab in view
new DropArea(new[] { typeof(GameObject) }, m_Views[(int)ViewIndex.First], (obj, localPos) =>
{
if (viewLayout == Layout.CustomSplit)
OnChangingObjectInViewInternal?.Invoke(obj as GameObject, ViewCompositionIndex.Composite, localPos);
else
OnChangingObjectInViewInternal?.Invoke(obj as GameObject, ViewCompositionIndex.First, localPos);
m_NoObject1.style.visibility = obj == null || obj.Equals(null) ? Visibility.Visible : Visibility.Hidden;
});
new DropArea(new[] { typeof(GameObject) }, m_Views[(int)ViewIndex.Second], (obj, localPos) =>
{
OnChangingObjectInViewInternal?.Invoke(obj as GameObject, ViewCompositionIndex.Second, localPos);
m_NoObject2.style.visibility = obj == null || obj.Equals(null) ? Visibility.Visible : Visibility.Hidden;
});
// Material in view
//new DropArea(new[] { typeof(GameObject) }, m_Views[(int)ViewIndex.First], (obj, localPos) =>
//{
// if (layout == Layout.CustomSplit || layout == Layout.CustomCircular)
// OnChangingMaterialInViewInternal?.Invoke(obj as Material, ViewCompositionIndex.Composite, localPos);
// else
// OnChangingMaterialInViewInternal?.Invoke(obj as Material, ViewCompositionIndex.First, localPos);
//});
//new DropArea(new[] { typeof(Material) }, m_Views[(int)ViewIndex.Second], (obj, localPos)
// => OnChangingMaterialInViewInternal?.Invoke(obj as Material, ViewCompositionIndex.Second, localPos));
// Environment in view
new DropArea(new[] { typeof(Environment), typeof(Cubemap) }, m_Views[(int)ViewIndex.First], (obj, localPos) =>
{
if (viewLayout == Layout.CustomSplit)
OnChangingEnvironmentInViewInternal?.Invoke(obj, ViewCompositionIndex.Composite, localPos);
else
OnChangingEnvironmentInViewInternal?.Invoke(obj, ViewCompositionIndex.First, localPos);
m_NoEnvironment1.style.visibility = obj == null || obj.Equals(null) ? Visibility.Visible : Visibility.Hidden;
});
new DropArea(new[] { typeof(Environment), typeof(Cubemap) }, m_Views[(int)ViewIndex.Second], (obj, localPos) =>
{
OnChangingEnvironmentInViewInternal?.Invoke(obj, ViewCompositionIndex.Second, localPos);
m_NoEnvironment2.style.visibility = obj == null || obj.Equals(null) ? Visibility.Visible : Visibility.Hidden;
});
// Environment in library
//new DropArea(new[] { typeof(Environment), typeof(Cubemap) }, m_EnvironmentContainer, (obj, localPos) =>
//{
// //[TODO: check if this come from outside of library]
// OnAddingEnvironmentInternal?.Invoke(obj);
//});
new DropArea(new[] { typeof(EnvironmentLibrary) }, m_EnvironmentContainer, (obj, localPos) =>
{
OnChangingEnvironmentLibraryInternal?.Invoke(obj as EnvironmentLibrary);
RefreshLibraryDisplay();
});
}
Rect IViewDisplayer.GetRect(ViewCompositionIndex index)
{
switch (index)
{
case ViewCompositionIndex.First:
case ViewCompositionIndex.Composite: //display composition on first rect
return m_Views[(int)ViewIndex.First].contentRect;
case ViewCompositionIndex.Second:
return m_Views[(int)ViewIndex.Second].contentRect;
default:
throw new ArgumentException("Unknown ViewCompositionIndex: " + index);
}
}
Vector2 m_LastFirstViewSize = new Vector2();
Vector2 m_LastSecondViewSize = new Vector2();
void IViewDisplayer.SetTexture(ViewCompositionIndex index, Texture texture)
{
if (texture == null)
return;
bool updated = false;
switch (index)
{
case ViewCompositionIndex.First:
case ViewCompositionIndex.Composite: //display composition on first rect
if (updated |= m_Views[(int)ViewIndex.First].image != texture)
m_Views[(int)ViewIndex.First].image = texture;
else if (updated |= (m_LastFirstViewSize.x != texture.width
|| m_LastFirstViewSize.y != texture.height))
{
m_Views[(int)ViewIndex.First].image = null; //force refresh else it will appear zoomed
m_Views[(int)ViewIndex.First].image = texture;
}
if (updated)
{
m_LastFirstViewSize.x = texture?.width ?? 0;
m_LastFirstViewSize.y = texture?.height ?? 0;
}
break;
case ViewCompositionIndex.Second:
if (m_Views[(int)ViewIndex.Second].image != texture)
m_Views[(int)ViewIndex.Second].image = texture;
else if (updated |= (m_LastSecondViewSize.x != texture.width
|| m_LastSecondViewSize.y != texture.height))
{
m_Views[(int)ViewIndex.Second].image = null; //force refresh else it will appear zoomed
m_Views[(int)ViewIndex.Second].image = texture;
}
if (updated)
{
m_LastSecondViewSize.x = texture?.width ?? 0;
m_LastSecondViewSize.y = texture?.height ?? 0;
}
break;
default:
throw new ArgumentException("Unknown ViewCompositionIndex: " + index);
}
}
void IViewDisplayer.Repaint() => Repaint();
void ApplyLayout(Layout value)
{
m_NoObject1.style.visibility = LookDev.currentContext.GetViewContent(ViewIndex.First).hasViewedObject ? Visibility.Hidden : Visibility.Visible;
m_NoObject2.style.visibility = LookDev.currentContext.GetViewContent(ViewIndex.Second).hasViewedObject ? Visibility.Hidden : Visibility.Visible;
m_NoEnvironment1.style.visibility = LookDev.currentContext.GetViewContent(ViewIndex.First).hasEnvironment ? Visibility.Hidden : Visibility.Visible;
m_NoEnvironment2.style.visibility = LookDev.currentContext.GetViewContent(ViewIndex.Second).hasEnvironment ? Visibility.Hidden : Visibility.Visible;
switch (value)
{
case Layout.HorizontalSplit:
case Layout.VerticalSplit:
if (!m_ViewContainer.ClassListContains(Style.k_FirstViewClass))
m_ViewContainer.AddToClassList(Style.k_FirstViewClass);
if (!m_ViewContainer.ClassListContains(Style.k_SecondViewsClass))
m_ViewContainer.AddToClassList(Style.k_SecondViewsClass);
if (value == Layout.VerticalSplit)
{
m_ViewContainer.AddToClassList(Style.k_VerticalViewsClass);
if (!m_ViewContainer.ClassListContains(Style.k_VerticalViewsClass))
m_ViewContainer.AddToClassList(Style.k_FirstViewClass);
}
for (int i = 0; i < 2; ++i)
m_Views[i].style.display = DisplayStyle.Flex;
break;
case Layout.FullFirstView:
case Layout.CustomSplit: //display composition on first rect
if (!m_ViewContainer.ClassListContains(Style.k_FirstViewClass))
m_ViewContainer.AddToClassList(Style.k_FirstViewClass);
if (m_ViewContainer.ClassListContains(Style.k_SecondViewsClass))
m_ViewContainer.RemoveFromClassList(Style.k_SecondViewsClass);
m_Views[0].style.display = DisplayStyle.Flex;
m_Views[1].style.display = DisplayStyle.None;
break;
case Layout.FullSecondView:
if (m_ViewContainer.ClassListContains(Style.k_FirstViewClass))
m_ViewContainer.RemoveFromClassList(Style.k_FirstViewClass);
if (!m_ViewContainer.ClassListContains(Style.k_SecondViewsClass))
m_ViewContainer.AddToClassList(Style.k_SecondViewsClass);
m_Views[0].style.display = DisplayStyle.None;
m_Views[1].style.display = DisplayStyle.Flex;
break;
default:
throw new ArgumentException("Unknown Layout");
}
//Add flex direction here
if (value == Layout.VerticalSplit)
m_ViewContainer.AddToClassList(Style.k_VerticalViewsClass);
else if (m_ViewContainer.ClassListContains(Style.k_VerticalViewsClass))
m_ViewContainer.RemoveFromClassList(Style.k_VerticalViewsClass);
}
void ApplySidePanelChange(SidePanel sidePanel)
{
IStyle GetEnvironmentContenairDraggerStyle()
=> m_EnvironmentContainer.Q(className: "unity-base-slider--vertical").Q("unity-dragger").style;
if (sidePanel == SidePanel.Environment)
{
if (!m_MainContainer.ClassListContains(Style.k_ShowEnvironmentPanelClass))
m_MainContainer.AddToClassList(Style.k_ShowEnvironmentPanelClass);
if (m_EnvironmentList.selectedIndex != -1)
m_EnvironmentContainer.Q<EnvironmentElement>().style.visibility = Visibility.Visible;
GetEnvironmentContenairDraggerStyle().display = DisplayStyle.Flex;
m_EnvironmentContainer.style.display = DisplayStyle.Flex;
}
else
{
if (m_MainContainer.ClassListContains(Style.k_ShowEnvironmentPanelClass))
m_MainContainer.RemoveFromClassList(Style.k_ShowEnvironmentPanelClass);
m_EnvironmentContainer.Q<EnvironmentElement>().style.visibility = Visibility.Hidden;
GetEnvironmentContenairDraggerStyle().display = DisplayStyle.None;
m_EnvironmentContainer.style.display = DisplayStyle.None;
}
if (sidePanel == SidePanel.Debug)
{
if (!m_MainContainer.ClassListContains(Style.k_ShowDebugPanelClass))
m_MainContainer.AddToClassList(Style.k_ShowDebugPanelClass);
UpdateSideDebugPanelProperties();
}
else
{
if (m_MainContainer.ClassListContains(Style.k_ShowDebugPanelClass))
m_MainContainer.RemoveFromClassList(Style.k_ShowDebugPanelClass);
}
}
void Update()
{
if (LookDev.waitingConfigure)
return;
// [case 1245086] Guard in case the SRP asset is set to null (or to a not supported SRP) when the lookdev window is already open
// Note: After an editor reload, we might get a null SRP for a couple of frames, hence the check.
if (!LookDev.supported)
{
// Print an error and close the Lookdev window (to avoid spamming the console)
if (RenderPipelineManager.currentPipeline != null)
Debug.LogError("LookDev is not supported by this Scriptable Render Pipeline: " + RenderPipelineManager.currentPipeline.ToString());
else if (GraphicsSettings.currentRenderPipeline != null)
Debug.LogError("LookDev is not available until a camera render occurs.");
else
Debug.LogError("LookDev is not supported: No SRP detected.");
LookDev.Close();
}
// All those states coming from the Contexts can become invalid after a domain reload so we need to update them.
m_FirstOrCompositeManipulator.UpdateCameraState(LookDev.currentContext);
m_SecondManipulator.UpdateCameraState(LookDev.currentContext, ViewIndex.Second);
m_GizmoManipulator.UpdateGizmoState(LookDev.currentContext.layout.gizmoState);
}
void OnGUI()
{
if (EditorApplication.isUpdating)
return;
//deal with missing style on domain reload...
ReloadStyleSheets();
OnUpdateRequestedInternal?.Invoke();
}
}
}

View File

@@ -0,0 +1,414 @@
.container
{
margin: 0px;
flex: 1;
flex-direction: row;
}
/* ///// */
/* VIEWS */
/* ///// */
/* override as later in document */
.verticalSplit
{
flex-direction: column;
}
#viewContainer
{
flex-shrink: 0;
}
#firstView,
#secondView
{
flex: 0;
}
/* override as later in document */
.firstView > #firstView,
.secondView > #secondView
{
flex: 1;
}
/* /////////// */
/* ENVIRONMENT */
/* /////////// */
#environmentContainer
{
width: 0px;
visibility: hidden;
flex-direction: column-reverse;
border-color: #232323;
border-left-width: 1px;
}
#debugContainer
{
width: 0px;
visibility: hidden;
border-color: #232323;
border-left-width: 1px;
}
#environmentContainer > EnvironmentElement
{
border-color: #232323;
flex-shrink: 0;
}
.showEnvironmentPanel > #environmentContainer
{
width: 255px;
visibility: visible;
}
.showDebugPanel > #debugContainer
{
width: 256px; /*219px;*/
visibility: visible;
}
.unity-label
{
min-width: 100px;
}
.unity-composite-field__field > .unity-base-field__label
{
padding-left: 0px;
min-width: 10px;
max-width: 10px;
}
.unity-composite-field__field > .unity-base-field__input
{
margin-left: 0px;
min-width: 38px;
max-width: 38px;
}
#unity-text-input
{
min-width: 40px;
}
.list-environment
{
flex: 1;
}
.list-environment-overlay
{
position: absolute;
bottom: 0px;
padding-left: 1px;
border-bottom-width: 0px;
background-color: rgba(0,0,0,0);
}
#environmentListCreationToolbar
{
padding-left: 1px;
}
#environmentListCreationToolbar > *
{
flex: 1;
-unity-text-align: middle-center;
}
#environmentListCreationToolbar > ObjectField > Label
{
min-width: 60px;
width: 60px;
}
#environmentListCreationToolbar > ToolbarButton
{
min-width: 40px;
width: 40px;
flex: 0;
border-left-width: 1px;
border-right-width: 0px;
margin-right: -1px;
}
ObjectFieldDisplay > Label
{
-unity-text-align: middle-left;
}
ObjectFieldDisplay > Image
{
min-width: 12px;
}
ToolbarButton
{
border-left-width: 0px;
}
.list-environment-overlay > ToolbarButton
{
border-width: 0px;
border-right-width: 1px;
width: 20px;
min-width: 20px;
-unity-text-align: middle-center;
background-color: #3c3c3c;
padding-left: 2px;
padding-right: 2px;
}
.list-environment-overlay > ToolbarButton:hover
{
background-color: #585858;
}
.list-environment-overlay > #duplicate
{
border-right-width: 0px;
}
Image.unity-list-view__item
{
width: 210px;
margin: 15px;
padding: 5px;
}
Image.unity-list-view__item:selected
{
border-width: 2px;
padding: 3px;
border-color: #3d6091;
background-color: rgba(0,0,0,0);
}
.sun-to-brightest-button
{
padding-left: 4px;
}
#inspector-header
{
flex-direction: row;
border-bottom-width: 1px;
border-color: #232323;
padding-top: 5px;
padding-bottom: 5px;
padding-left: 3px;
background-color: #3C3C3C;
}
#inspector-header > Image
{
margin-top: 2px;
margin-bottom: 2px;
}
#inspector-header > TextField
{
flex: 1;
}
#inspector
{
padding-bottom: 5px;
}
#separator-line
{
background-color: #3C3C3C;
}
#separator
{
flex: 1;
width: 188px;
border-bottom-width: 1px;
border-color: #232323;
height: 0;
align-self: flex-end;
}
#sunToBrightestButton
{
background-color: rgba(0,0,0,0);
border-radius: 0px;
border-width: 0px;
padding: 0px;
margin-right: 1px;
}
#sunToBrightestButton:hover
{
background-color: #585858;
}
/* /////// */
/* /DEBUG/ */
/* /////// */
MultipleDifferentValue
{
-unity-font-style: bold;
font-size: 15px;
-unity-text-align: middle-center;
margin-bottom: 2px;
height: 15px;
}
MultipleSourcePopupField > MultipleDifferentValue
{
-unity-text-align: middle-left;
width: 120px;
background-color: #515151;
position: absolute;
left: 103px;
bottom: 1px;
}
MultipleSourcePopupField > MultipleDifferentValue:hover
{
background-color: #585858;
}
#debugToolbar
{
margin-top: 16px;
margin-bottom: 16px;
flex-direction: row;
align-self: center;
border-bottom-width: 1px;
border-top-width: 1px;
border-color: #232323;
}
#debugToolbar > ToolbarToggle
{
width: 40px;
left: 0px;
}
#debugToolbar > ToolbarToggle > *
{
justify-content: center;
}
/* /////// */
/* TOOLBAR */
/* /////// */
#toolbar
{
flex-direction: row;
}
#toolbarRadio
{
flex-direction: row;
}
.unity-toggle__input > .unity-image
{
padding: 2px;
}
#tabsRadio
{
width: 256px;
min-width: 256px;
max-width: 256px;
flex: 1;
flex-direction: row;
-unity-text-align: middle-center;
}
#tabsRadio > ToolbarToggle
{
flex: 1;
left: 0px;
}
#tabsRadio > ToolbarToggle > * > Label
{
flex: 1;
}
.unity-toolbar-toggle
{
padding-top: 0px;
padding-right: 0px;
padding-bottom: 0px;
padding-left: 0px;
margin-left: 0px;
margin-right: 0px;
border-left-width: 0px;
}
#renderdoc-content
{
flex: 1;
flex-direction: row;
max-width: 80px;
flex-shrink: 0;
min-width: 24px;
border-left-width: 1px;
}
#renderdoc-content > Label
{
-unity-text-align: middle-left;
min-width: 0px;
padding-top: 0px;
}
#renderdoc-content > Image
{
flex-shrink: 0;
min-width: 16px;
min-height: 16px;
}
#cameraMenu
{
flex-direction: row-reverse;
padding: 0px;
}
#cameraButton
{
border-radius: 0px;
border-width: 0px;
border-left-width: 1px;
padding: 0px;
padding-right: 4px;
margin: 0px;
}
#cameraSeparator
{
margin-top: 4px;
margin-bottom: 4px;
border-right-width: 1px;
}
#cameraButton > *
{
margin: 2px;
}
/* /////////// */
/* DRAG'N'DROP */
/* /////////// */
#cursorFollower
{
position: absolute;
}

View File

@@ -0,0 +1,103 @@
using System.Collections;
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UnityEditor.Rendering.LookDev
{
class DropArea
{
readonly Type[] k_AcceptedTypes;
bool droppable;
public DropArea(Type[] acceptedTypes, VisualElement area, Action<UnityEngine.Object, Vector2> OnDrop)
{
k_AcceptedTypes = acceptedTypes;
area.RegisterCallback<DragPerformEvent>(evt => Drop(evt, OnDrop));
area.RegisterCallback<DragEnterEvent>(evt => DragEnter(evt));
area.RegisterCallback<DragLeaveEvent>(evt => DragLeave(evt));
area.RegisterCallback<DragExitedEvent>(evt => DragExit(evt));
area.RegisterCallback<DragUpdatedEvent>(evt => DragUpdate(evt));
}
void DragEnter(DragEnterEvent evt)
{
droppable = false;
foreach (UnityEngine.Object obj in DragAndDrop.objectReferences)
{
if (!IsInAcceptedTypes(obj.GetType()))
continue;
droppable = true;
evt.StopPropagation();
return;
}
}
void DragLeave(DragLeaveEvent evt)
{
foreach (UnityEngine.Object obj in DragAndDrop.objectReferences)
{
if (!IsInAcceptedTypes(obj.GetType()))
continue;
DragAndDrop.visualMode = DragAndDropVisualMode.Rejected;
evt.StopPropagation();
return;
}
}
void DragExit(DragExitedEvent evt)
{
foreach (UnityEngine.Object obj in DragAndDrop.objectReferences)
{
if (!IsInAcceptedTypes(obj.GetType()))
continue;
DragAndDrop.visualMode = DragAndDropVisualMode.Rejected;
evt.StopPropagation();
return;
}
}
void DragUpdate(DragUpdatedEvent evt)
{
foreach (UnityEngine.Object obj in DragAndDrop.objectReferences)
{
if (!IsInAcceptedTypes(obj.GetType()))
continue;
DragAndDrop.visualMode = droppable ? DragAndDropVisualMode.Link : DragAndDropVisualMode.Rejected;
evt.StopPropagation();
}
}
void Drop(DragPerformEvent evt, Action<UnityEngine.Object, Vector2> OnDrop)
{
bool atLeastOneAccepted = false;
foreach (UnityEngine.Object obj in DragAndDrop.objectReferences)
{
if (!IsInAcceptedTypes(obj.GetType()))
continue;
OnDrop.Invoke(obj, evt.localMousePosition);
atLeastOneAccepted = true;
}
if (atLeastOneAccepted)
{
DragAndDrop.AcceptDrag();
evt.StopPropagation();
}
}
bool IsInAcceptedTypes(Type testedType)
{
foreach (Type type in k_AcceptedTypes)
{
if (testedType.IsAssignableFrom(type))
return true;
}
return false;
}
}
}

View File

@@ -0,0 +1,467 @@
using UnityEngine;
using System;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
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, 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.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, RenderTextureFormat.ARGB32, RenderTextureReadWrite.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, 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.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();
}
}
}

View File

@@ -0,0 +1,207 @@
using UnityEngine;
using UnityEngine.Rendering;
using System;
using System.Collections.Generic;
using UnityEngine.UIElements;
using System.IO;
using UnityEditor;
using UnityEditor.UIElements;
namespace UnityEditor.Rendering.LookDev
{
/// <summary>
/// Class containing a collection of Environment
/// </summary>
[HelpURL(Documentation.baseURLHDRP + Documentation.version + Documentation.subURL + "Environment-Library" + Documentation.endURL)]
public class EnvironmentLibrary : ScriptableObject
{
[field: SerializeField]
List<Environment> environments { get; set; } = new List<Environment>();
/// <summary>
/// Number of elements in the collection
/// </summary>
public int Count => environments.Count;
/// <summary>
/// Indexer giving access to contained Environment
/// </summary>
public Environment this[int index] => environments[index];
/// <summary>
/// Create a new empty Environment at the end of the collection
/// </summary>
/// <returns>The created Environment</returns>
public Environment Add()
{
Undo.SetCurrentGroupName("Add Environment");
int group = Undo.GetCurrentGroup();
Environment environment = ScriptableObject.CreateInstance<Environment>();
environment.name = "New Environment";
Undo.RegisterCreatedObjectUndo(environment, "Add Environment");
Undo.RecordObject(this, "Add Environment");
environments.Add(environment);
// Store this new environment as a subasset so we can reference it safely afterwards.
AssetDatabase.AddObjectToAsset(environment, this);
Undo.CollapseUndoOperations(group);
// Force save / refresh. Important to do this last because SaveAssets can cause effect to become null!
EditorUtility.SetDirty(this);
AssetDatabase.SaveAssets();
return environment;
}
/// <summary>
/// Remove Environment of the collection at given index
/// </summary>
/// <param name="index">Index where to remove Environment</param>
public void Remove(int index)
{
Undo.SetCurrentGroupName("Remove Environment");
int group = Undo.GetCurrentGroup();
Environment environment = environments[index];
Undo.RecordObject(this, "Remove Environment");
environments.RemoveAt(index);
Undo.DestroyObjectImmediate(environment);
Undo.CollapseUndoOperations(group);
// Force save / refresh
EditorUtility.SetDirty(this);
AssetDatabase.SaveAssets();
}
/// <summary>
/// Duplicate the Environment at given index and add it at the end of the Collection
/// </summary>
/// <param name="fromIndex">Index where to take data for duplication</param>
/// <returns>The created Environment</returns>
public Environment Duplicate(int fromIndex)
{
Undo.SetCurrentGroupName("Duplicate Environment");
int group = Undo.GetCurrentGroup();
Environment environment = ScriptableObject.CreateInstance<Environment>();
Environment environmentToCopy = environments[fromIndex];
environmentToCopy.CopyTo(environment);
Undo.RegisterCreatedObjectUndo(environment, "Duplicate Environment");
Undo.RecordObject(this, "Duplicate Environment");
environments.Add(environment);
// Store this new environment as a subasset so we can reference it safely afterwards.
AssetDatabase.AddObjectToAsset(environment, this);
Undo.CollapseUndoOperations(group);
// Force save / refresh. Important to do this last because SaveAssets can cause effect to become null!
EditorUtility.SetDirty(this);
AssetDatabase.SaveAssets();
return environment;
}
/// <summary>
/// Compute position of given Environment in the collection
/// </summary>
/// <param name="environment">Environment to look at</param>
/// <returns>Index of the searched environment. If not found, -1.</returns>
public int IndexOf(Environment environment)
=> environments.IndexOf(environment);
}
[CustomEditor(typeof(EnvironmentLibrary))]
class EnvironmentLibraryEditor : Editor
{
VisualElement m_Root;
VisualElement m_OpenButton;
public sealed override VisualElement CreateInspectorGUI()
{
var library = target as EnvironmentLibrary;
m_Root = new VisualElement();
m_OpenButton = new Button(() =>
{
if (!LookDev.open)
LookDev.Open();
LookDev.currentContext.UpdateEnvironmentLibrary(library);
LookDev.currentEnvironmentDisplayer.Repaint();
})
{
text = "Open in Look Dev window"
};
m_OpenButton.SetEnabled(LookDev.supported);
m_Root.Add(m_OpenButton);
return m_Root;
}
void OnEnable() => EditorApplication.update += Update;
void OnDisable() => EditorApplication.update -= Update;
void Update()
{
// Current SRP can be changed at any time so we need to do this at every update.
if (m_OpenButton != null)
m_OpenButton.SetEnabled(LookDev.supported);
}
// Don't use ImGUI
public sealed override void OnInspectorGUI() {}
}
class EnvironmentLibraryCreator : ProjectWindowCallback.EndNameEditAction
{
ObjectField m_Field = null;
public void SetField(ObjectField field)
=> m_Field = field;
public override void Cancelled(int instanceId, string pathName, string resourceFile)
=> m_Field = null;
public override void Action(int instanceId, string pathName, string resourceFile)
{
var newAsset = CreateInstance<EnvironmentLibrary>();
newAsset.name = Path.GetFileName(pathName);
AssetDatabase.CreateAsset(newAsset, pathName);
ProjectWindowUtil.ShowCreatedAsset(newAsset);
if (m_Field != null)
m_Field.value = newAsset;
m_Field = null;
}
[MenuItem("Assets/Create/LookDev/Environment Library", priority = 2000)]
static void Create()
{
var icon = EditorGUIUtility.FindTexture("ScriptableObject Icon");
ProjectWindowUtil.StartNameEditingIfProjectWindowExists(0, ScriptableObject.CreateInstance<EnvironmentLibraryCreator>(), "New EnvironmentLibrary.asset", icon, null);
}
public static void CreateAndAssignTo(ObjectField field)
{
var icon = EditorGUIUtility.FindTexture("ScriptableObject Icon");
var assetCreator = ScriptableObject.CreateInstance<EnvironmentLibraryCreator>();
assetCreator.SetField(field);
ProjectWindowUtil.StartNameEditingIfProjectWindowExists(assetCreator.GetInstanceID(), assetCreator, "New EnvironmentLibrary.asset", icon, null);
}
}
static class EnvironmentLibraryLoader
{
static Action<UnityEngine.Object> LoadCallback(Action onUpdate)
{
return (UnityEngine.Object newLibrary) =>
{
LookDev.currentContext.UpdateEnvironmentLibrary(newLibrary as EnvironmentLibrary);
onUpdate?.Invoke();
};
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 891 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 B

Some files were not shown because too many files have changed in this diff Show More