Last active
January 31, 2023 14:17
-
-
Save cjacobwade/baa610bf92cb0a1d5c373710ef1826f8 to your computer and use it in GitHub Desktop.
This is MIT license and will be part of BigHopsTools when I get around to it
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections; | |
using System.Collections.Generic; | |
using UnityEngine; | |
using System.Reflection; | |
#if UNITY_EDITOR | |
using UnityEditor; | |
#endif | |
[CreateAssetMenu(fileName = "SceneVisualsData", menuName ="Luckshot/Scene Visuals Data")] | |
public class SceneVisualsData : ScriptableObject | |
{ | |
[Header("Skybox")] | |
public Gradient skyboxGradient = new Gradient(); | |
public Texture2D skyboxTexture = null; | |
public Material skyboxMaterial = null; | |
public Color ambientColor = new Color(155f/255f, 171f/255f, 204f/255f, 1f); | |
[Header("Sun")] | |
public float sunRadius = 0.2f; | |
public float sunFalloff = 0.1f; | |
public Color sunColor = Color.white; | |
public Color shadowColor = Color.white; | |
[Header("Cloud")] | |
public Color cloudColor = Color.white; | |
public Color cloudColor2 = new Color(0.8f, 0.8f, 0.8f, 1f); | |
public Material cloudMaterial = null; | |
[Header("Fog")] | |
public Color fogColor = new Color(198f/255f, 241f/255f, 1f, 1f); | |
public FogMode fogMode = FogMode.ExponentialSquared; | |
public float fogDensity = 0.01f; | |
public float fogStartDistance = 0f; | |
public float fogEndDistance = 200f; | |
// Cacheing these and doing material.SetColor(int) instead of material.SetColor(string) | |
// is faster and using readonly here lets you call Shader.PropertyToID inline | |
private readonly int sunColorPropID = Shader.PropertyToID("_Sun"); | |
private readonly int sunRadiusPropID = Shader.PropertyToID("_SunRadius"); | |
private readonly int sunFalloffPropID = Shader.PropertyToID("_SunFalloff"); | |
private readonly int cloudColorPropID = Shader.PropertyToID("_Color"); | |
private readonly int cloudColor2PropID = Shader.PropertyToID("_Color2"); | |
private readonly int shadowColorPropID = Shader.PropertyToID("_GSColor"); | |
public void ApplySettings() | |
{ | |
RenderSettings.skybox = skyboxMaterial; | |
RenderSettings.ambientSkyColor = ambientColor; | |
RenderSettings.fogMode = fogMode; | |
RenderSettings.fogDensity = fogDensity; | |
RenderSettings.fogStartDistance = fogStartDistance; | |
RenderSettings.fogEndDistance = fogEndDistance; | |
RenderSettings.fogColor = fogColor; | |
skyboxMaterial.SetColor(sunColorPropID, sunColor); | |
skyboxMaterial.SetFloat(sunRadiusPropID, sunRadius); | |
skyboxMaterial.SetFloat(sunFalloffPropID, sunFalloff); | |
cloudMaterial.SetColor(cloudColorPropID, cloudColor); | |
cloudMaterial.SetColor(cloudColor2PropID, cloudColor2); | |
// This method lets you set a fallback property that all shaders can access | |
// If a shader implements a property with the same name the inspector value overrides this | |
Shader.SetGlobalColor(shadowColorPropID, shadowColor); | |
// For scene specific things like clouds this is the best solution I've found. | |
// In the future might make a ISceneVisuals interface that classes can implement | |
// rather than having this script know about game specific scripts | |
CloudLayer[] clouds = FindObjectsOfType<CloudLayer>(); | |
for (int i = 0; i < clouds.Length; i++) | |
{ | |
MeshRenderer cloudMR = clouds[i].GetComponent<MeshRenderer>(); | |
if (cloudMR != null) | |
cloudMR.sharedMaterial = cloudMaterial; | |
} | |
} | |
public void CacheFromScene() | |
{ | |
ambientColor = RenderSettings.ambientSkyColor; | |
shadowColor = Shader.GetGlobalColor(shadowColorPropID); | |
fogMode = RenderSettings.fogMode; | |
fogDensity = RenderSettings.fogDensity; | |
fogStartDistance = RenderSettings.fogStartDistance; | |
fogEndDistance = RenderSettings.fogEndDistance; | |
fogColor = RenderSettings.fogColor; | |
skyboxMaterial = RenderSettings.skybox; | |
if (skyboxMaterial != null) | |
{ | |
sunColor = skyboxMaterial.GetColor(sunColorPropID); | |
sunRadius = skyboxMaterial.GetFloat(sunRadiusPropID); | |
sunFalloff = skyboxMaterial.GetFloat(sunFalloffPropID); | |
} | |
cloudMaterial = null; | |
CloudLayer[] clouds = FindObjectsOfType<CloudLayer>(); | |
for (int i = 0; i < clouds.Length; i++) | |
{ | |
MeshRenderer cloudMR = clouds[i].GetComponent<MeshRenderer>(); | |
if (cloudMR != null) | |
cloudMaterial = cloudMR.sharedMaterial; | |
} | |
if (cloudMaterial != null) | |
{ | |
cloudColor = cloudMaterial.GetColor(cloudColorPropID); | |
cloudColor2 = cloudMaterial.GetColor(cloudColor2PropID); | |
} | |
} | |
public void Copy(SceneVisualsData other) | |
{ | |
// This is to copy all fields from one SceneVisualsData instance to this one | |
// Using reflection for this means I don't have to worry about forgetting to add to this function | |
// as I add more fields to this class | |
FieldInfo[] fields = typeof(SceneVisualsData).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); | |
for(int i = 0; i < fields.Length; i++) | |
fields[i].SetValue(this, fields[i].GetValue(other)); | |
#if UNITY_EDITOR | |
// Need this to make sure SaveProject serializes changes caused by copying | |
EditorUtility.SetDirty(this); | |
#endif | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections; | |
using System.Collections.Generic; | |
using UnityEngine; | |
using UnityEditor; | |
using System.IO; | |
using UnityEditor.SceneManagement; | |
[CustomEditor(typeof(SceneVisualsData))] | |
public class SceneVisualsDataEditor : Editor | |
{ | |
private SceneVisualsData visData | |
{ get { return target as SceneVisualsData; } } | |
private bool gradientDirty = false; | |
private readonly int gradientTexWidth = 1024; | |
private SceneVisualsData cachedVisData = null; | |
private readonly int mainTexPropID = Shader.PropertyToID("_MainTex"); | |
// For editor scripts OnEnable is called when the object is selected and the inspector opens | |
// OnDisable is called when the object is deselected and the inspector closes | |
// This means you can treat OnEnable / OnDisable as initialization and deinitialization of | |
// anything you need for your custom editor and that you can safely register callbacks here | |
private void OnEnable() | |
{ | |
Undo.undoRedoPerformed += OnUndoRedoPerformed; | |
// Save whatever the current relevant settings to a proxy SceneVisualsData asset | |
// so we can see the newly selected assets settings as applied to the scene while tweaking | |
// and (importantly) be able to safely revert when this asset is deselected | |
CacheSettings(); | |
if (visData.skyboxMaterial == null) | |
{ | |
visData.skyboxMaterial = new Material(Shader.Find("Luckshot/ProceduralSkybox")); | |
visData.skyboxMaterial.name = visData.name + "-Skybox"; | |
if (cachedVisData != null && cachedVisData.cloudMaterial != null) | |
visData.skyboxMaterial.CopyPropertiesFromMaterial(cachedVisData.skyboxMaterial); | |
// This method saves the skybox material as part of SceneVisualsData asset | |
// which keeps them nicely organized together | |
AssetDatabase.AddObjectToAsset(visData.skyboxMaterial, visData); | |
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(visData.skyboxMaterial)); | |
EditorUtility.SetDirty(visData); | |
} | |
if (visData.cloudMaterial == null) | |
{ | |
visData.cloudMaterial = new Material(Shader.Find("Luckshot/CloudLayer")); | |
visData.cloudMaterial.name = visData.name + "-Cloud"; | |
if (cachedVisData != null && cachedVisData.cloudMaterial != null) | |
visData.cloudMaterial.CopyPropertiesFromMaterial(cachedVisData.cloudMaterial); | |
AssetDatabase.AddObjectToAsset(visData.cloudMaterial, visData); | |
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(visData.cloudMaterial)); | |
EditorUtility.SetDirty(visData); | |
} | |
visData.ApplySettings(); | |
} | |
private void OnDisable() | |
{ | |
Undo.undoRedoPerformed -= OnUndoRedoPerformed; | |
if (cachedVisData != null) | |
{ | |
cachedVisData.ApplySettings(); | |
DestroyImmediate(cachedVisData); | |
} | |
if (gradientDirty && visData.skyboxTexture != null) | |
{ | |
string path = AssetDatabase.GetAssetPath(visData); | |
path = path.Replace(".asset", ".png"); | |
// This (for some reason) is the correct way to save a Texture2D to | |
// disk so it can be treated like other texture assets. Note that you have | |
// to manually set the ending of the file path to .png or reimporting will fail | |
byte[] bytes = visData.skyboxTexture.EncodeToPNG(); | |
File.WriteAllBytes(path, bytes); | |
AssetDatabase.ImportAsset(path); | |
AssetDatabase.Refresh(); | |
// This lets us change the import settings on the texture in code | |
// so we can apply the same settings for all skybox textures with having | |
// to do it manually | |
TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter; | |
importer.wrapMode = TextureWrapMode.Clamp; | |
importer.textureCompression = TextureImporterCompression.CompressedHQ; | |
importer.mipmapEnabled = false; | |
importer.maxTextureSize = visData.skyboxTexture.width; | |
AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate); | |
visData.skyboxTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(path); | |
visData.skyboxMaterial.SetTexture(mainTexPropID, visData.skyboxTexture); | |
EditorUtility.SetDirty(visData); | |
gradientDirty = false; | |
} | |
} | |
private void OnUndoRedoPerformed() | |
{ | |
// When we undo make sure that change is visually reflected | |
visData.ApplySettings(); | |
} | |
private void CacheSettings() | |
{ | |
if (cachedVisData == null) | |
cachedVisData = ScriptableObject.CreateInstance<SceneVisualsData>(); | |
cachedVisData.CacheFromScene(); | |
} | |
public override void OnInspectorGUI() | |
{ | |
// For safely undoing stuff, you need to record the objects before any changes get made | |
// In a custom editor like this you don't know until the user makes an edit if you need to undo | |
// So every inspector update I save the object as if I'm going to make a change then | |
// If I make it to the end of this function without having made a change I remove the undo record | |
Undo.RecordObject(visData, "Changed VisData"); | |
if (string.IsNullOrEmpty(visData.name)) | |
visData.name = typeof(SceneVisualsData).ToString(); | |
EditorGUILayout.LabelField("Skybox", EditorStyles.boldLabel); | |
EditorGUI.BeginChangeCheck(); | |
visData.skyboxGradient = EditorGUILayout.GradientField("Gradient", visData.skyboxGradient); | |
if(EditorGUI.EndChangeCheck()) | |
{ | |
// This function creates a texture based on the passed in gradient | |
// It's what you'd expect, just storing gradient.Evaluate(pixelX/gradientTexWidth) in the horizontal pixels | |
// Note that this isn't saved to an asset here because that's very slow. We only do that in OnDisable when | |
// this object is deselected after having the gradientDirty flag set | |
visData.skyboxTexture = ColorUtils.CreateGradientTexture(visData.skyboxGradient, gradientTexWidth); | |
visData.skyboxTexture.name = visData.name + "-SkyboxGradient"; | |
if (visData.skyboxMaterial != null) | |
visData.skyboxMaterial.SetTexture(mainTexPropID, visData.skyboxTexture); | |
gradientDirty = true; | |
return; | |
} | |
EditorGUI.BeginChangeCheck(); | |
visData.ambientColor = EditorGUILayout.ColorField("Ambient Color", visData.ambientColor); | |
EditorGUILayout.LabelField("Sun", EditorStyles.boldLabel); | |
visData.sunColor = EditorGUILayout.ColorField("Color", visData.sunColor); | |
visData.sunRadius = EditorGUILayout.FloatField("Radius", visData.sunRadius); | |
visData.sunFalloff = EditorGUILayout.FloatField("Falloff", visData.sunFalloff); | |
visData.shadowColor = EditorGUILayout.ColorField("Shadow Color", visData.shadowColor); | |
EditorGUILayout.LabelField("Cloud", EditorStyles.boldLabel); | |
visData.cloudColor = EditorGUILayout.ColorField("Top Color", visData.cloudColor); | |
visData.cloudColor2 = EditorGUILayout.ColorField("Bottom Color", visData.cloudColor2); | |
EditorGUILayout.LabelField("Fog", EditorStyles.boldLabel); | |
visData.fogMode = (FogMode)EditorGUILayout.EnumPopup("Fog Mode", visData.fogMode); | |
visData.fogColor = EditorGUILayout.ColorField("Color", visData.fogColor); | |
if(visData.fogMode == FogMode.Linear) | |
{ | |
visData.fogStartDistance = EditorGUILayout.FloatField("Fog Start Distance", visData.fogStartDistance); | |
visData.fogEndDistance = EditorGUILayout.FloatField("Fog End Distance", visData.fogEndDistance); | |
} | |
else | |
visData.fogDensity = EditorGUILayout.FloatField("Radius", visData.fogDensity); | |
if (EditorGUI.EndChangeCheck()) | |
{ | |
visData.ApplySettings(); | |
return; | |
} | |
// If we make it this far the user has changed nothing so remove our object from the undo stack | |
Undo.ClearUndo(visData); | |
if (GUILayout.Button("Save Settings")) | |
SaveSettings(); | |
} | |
private void SaveSettings() | |
{ | |
// This is a hack that calls the internal unity function RenderSettings.GetRenderSettings() | |
// which retuns a reference to the RenderSettings asset so we can record changes to it | |
Object renderSettings = (Object)ReflectionUtils.DoInvoke(typeof(RenderSettings), "GetRenderSettings", new object[0]); | |
Undo.RecordObject(renderSettings, "Render Settings"); | |
// Here were making sure the visuals are applied to the scene visually | |
// then we replace our cached settings with the newly applied settings | |
visData.ApplySettings(); | |
CacheSettings(); | |
// Since these settings affect RenderSettings and potentially some components that | |
// live in the scene, let's make sure the scene gets marked dirty | |
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene()); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment