Skip to content

Instantly share code, notes, and snippets.

@cjacobwade
Last active January 31, 2023 14:17
Show Gist options
  • Save cjacobwade/baa610bf92cb0a1d5c373710ef1826f8 to your computer and use it in GitHub Desktop.
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
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
}
}
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