Skip to content

Instantly share code, notes, and snippets.

@Farfarer
Last active July 2, 2024 06:54
Show Gist options
  • Save Farfarer/5664694 to your computer and use it in GitHub Desktop.
Save Farfarer/5664694 to your computer and use it in GitHub Desktop.
Create dynamic equirectangular maps for Unity. These have the benefit that, as they're flat images, you can sample lower mips to get blurry reflections. The straight cubemap version (detailed here: http://www.farfarer.com/blog/2011/07/25/dynamic-ambient-lighting-in-unity/ ) will give you hard seams when you sample mips from the cubemap. Although…
// This takes in the cubemap generated by your cubemap camera and feeds back out an equirectangular image.
// Create a new material and give it this shader. Then give that material to the "cubemapToEquirectangularMateral" property of the dynamicAmbient.js script in this gist.
// You could probably abstract this to C#/JS code and feed it in a pre-baked cubemap to sample and then spit out an equirectangular map if you don't have render textures.
Shader "Custom/cubemapToEquirectangular" {
Properties {
_MainTex ("Cubemap (RGB)", CUBE) = "" {}
}
Subshader {
Pass {
ZTest Always Cull Off ZWrite Off
Fog { Mode off }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma fragmentoption ARB_precision_hint_fastest
#include "UnityCG.cginc"
struct v2f {
float4 pos : POSITION;
float2 uv : TEXCOORD0;
};
samplerCUBE _MainTex;
#define PI 3.141592653589793
#define HALFPI 1.57079632679
v2f vert( appdata_img v )
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
float2 uv = v.texcoord.xy * 2 - 1;
uv *= float2(PI, HALFPI);
o.uv = uv;
return o;
}
fixed4 frag(v2f i) : COLOR
{
float cosy = cos(i.uv.y);
float3 normal = float3(0,0,0);
normal.x = cos(i.uv.x) * cosy;
normal.y = i.uv.y;
normal.z = cos(i.uv.x - HALFPI) * cosy;
return texCUBE(_MainTex, normal);
}
ENDCG
}
}
Fallback Off
}
// Wizard to convert a cubemap to an equirectangular cubemap.
// Put this into an /Editor folder
// Run it from Tools > Cubemap to Equirectangular Map
using UnityEditor;
using UnityEngine;
using System.IO;
class CubemapToEquirectangularWizard : ScriptableWizard {
public Cubemap cubeMap = null;
public int equirectangularWidth = 2048;
public int equirectangularHeight = 1024;
private Material cubemapToEquirectangularMaterial;
private Shader cubemapToEquirectangularShader;
[MenuItem ("Tools/Cubemap to Equirectangular Map")]
static void CreateWizard () {
ScriptableWizard.DisplayWizard<CubemapToEquirectangularWizard>("Cubemap to Equirectangular Map", "Convert");
}
void OnWizardCreate () {
bool goodToGo = true;
cubemapToEquirectangularShader = Shader.Find("Custom/cubemapToEquirectangular");
if ( cubemapToEquirectangularShader == null )
{
Debug.LogWarning ( "Couldn't find the shader \"Custom/cubemapToEquirectangular\", do you have it in your project?\nYou can get it here; https://gist.github.com/Farfarer/5664694#file-cubemaptoequirectangular-shader");
goodToGo = false;
}
else {
cubemapToEquirectangularMaterial = new Material( cubemapToEquirectangularShader );
}
if ( cubeMap == null )
{
Debug.LogWarning ( "You must specify a cubemap.");
goodToGo = false;
}
else if ( equirectangularWidth < 1 )
{
Debug.LogWarning ( "Width must be greater than 0.");
goodToGo = false;
}
else if ( equirectangularHeight < 1 )
{
Debug.LogWarning ( "Height must be greater than 0.");
goodToGo = false;
}
if (goodToGo) {
// Go to gamma space.
ColorSpace originalColorSpace = PlayerSettings.colorSpace;
PlayerSettings.colorSpace = ColorSpace.Gamma;
// Do the conversion.
RenderTexture rtex_equi = new RenderTexture ( equirectangularWidth, equirectangularHeight, 24 );
Graphics.Blit (cubeMap, rtex_equi, cubemapToEquirectangularMaterial);
Texture2D equiMap = new Texture2D(equirectangularWidth, equirectangularHeight, TextureFormat.ARGB32, false);
//equiMap.SetPixels(rtex_equiPixels);
equiMap.ReadPixels(new Rect(0, 0, equirectangularWidth, equirectangularHeight), 0, 0, false);
equiMap.Apply();
byte[] bytes = equiMap.EncodeToPNG();
DestroyImmediate(equiMap);
string assetPath = AssetDatabase.GetAssetPath(cubeMap);
string assetDir = Path.GetDirectoryName(assetPath);
string assetName = Path.GetFileNameWithoutExtension(assetPath) + "_equirectangular.png";
string newAsset = Path.Combine(assetDir, assetName);
File.WriteAllBytes(newAsset, bytes);
// Import the new texture.
AssetDatabase.ImportAsset(newAsset);
Debug.Log ("Equirectangular map saved to " + newAsset);
// Go to whatever the color space was before.
PlayerSettings.colorSpace = originalColorSpace;
}
}
void OnWizardUpdate () {
helpString = "Converts a cubemap into an equirectangular map.";
}
}
// Apply this script to the camera you'll use to generate your cubemaps.
@script ExecuteInEditMode
private var cam : Camera;
public var target : Transform;
private var tr : Transform;
public var cubemapSize : int = 512;
public var oneFacePerFrame : boolean = false;
public var offset : Vector3 = Vector3.zero;
private var rtex : RenderTexture;
public var createEquirectangularMap = true;
public var equirectangularSize : int = 1024;
public var cubemapToEquirectangularMateral : Material = null;
private var rtex_equi : RenderTexture;
function Start () {
cam = camera;
cam.enabled = false;
// render all six faces at startup
UpdateCubemap( 63 );
}
function LateUpdate () {
if ( oneFacePerFrame ) {
var faceToRender = Time.frameCount % 6;
var faceMask = 1 << faceToRender;
UpdateCubemap ( faceMask );
} else {
UpdateCubemap ( 63 ); // all six faces
}
}
function UpdateCubemap ( faceMask : int ) {
if ( !tr ) {
tr = transform;
tr.rotation = Quaternion.identity;
}
if ( !rtex ) {
rtex = new RenderTexture ( cubemapSize, cubemapSize, 16 );
rtex.isPowerOfTwo = true;
rtex.isCubemap = true;
rtex.useMipMap = false;
rtex.hideFlags = HideFlags.HideAndDontSave;
rtex.SetGlobalShaderProperty ( "_WorldCube" );
}
tr.position = target.position + offset;
cam.RenderToCubemap ( rtex, faceMask );
if ( !rtex_equi ) {
rtex_equi = new RenderTexture ( equirectangularSize * 2, equirectangularSize, 16 );
rtex_equi.isPowerOfTwo = true;
rtex_equi.isCubemap = false;
rtex_equi.useMipMap = true;
rtex_equi.wrapMode = TextureWrapMode.Repeat;
rtex_equi.hideFlags = HideFlags.HideAndDontSave;
rtex_equi.filterMode = FilterMode.Trilinear;
rtex_equi.SetGlobalShaderProperty ( "_Equirectangular" );
var mipNum : float = Mathf.Ceil(Mathf.Log(equirectangularSize * 2)) + 1;
Shader.SetGlobalFloat("_EquirectangularBlur", mipNum);
}
if (createEquirectangularMap) {
Graphics.Blit (rtex, rtex_equi, cubemapToEquirectangularMateral);
}
}
function OnDisable () {
DestroyImmediate ( rtex );
DestroyImmediate ( rtex_equi );
}
// This shader samples the equirectangular map to get an ambient and a specular value from it.
Shader "Custom/Equirectangular/DynamicAmbient" {
Properties {
_Color ("Main Color", Color) = (1,1,1,1)
_MainTex ("Diffuse (RGB) Alpha (A)", 2D) = "gray" {}
_SpecularTex ("Specular (R) Gloss (B) Fresnel (B)", 2D) = "gray" {}
_BumpMap ("Normal (Normal)", 2D) = "bump" {}
}
SubShader{
Pass {
Tags {"LightMode" = "Always"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma fragmentoption ARB_precision_hint_fastest
#pragma glsl
#pragma target 3.0
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float4 tangent : TANGENT;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normal : TEXCOORD2;
float3 tangent : TEXCOORD3;
float3 binormal : TEXCOORD4;
float3 viewDir : TEXCOORD5;
};
v2f vert (appdata v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = v.texcoord.xy;
o.normal = v.normal;
o.tangent = v.tangent;
o.binormal = cross(v.normal, v.tangent.xyz) * v.tangent.w;
o.viewDir = WorldSpaceViewDir(v.vertex);
return o;
}
sampler2D _MainTex, _SpecularTex, _BumpMap;
sampler2D _Equirectangular;
float _EquirectangularBlur;
#define PI 3.141592653589793
inline float3 TangentToWorld(float3 tSpace, float3 normal, float3 tangent, float3 binormal)
{
float3 normalO = (tangent * tSpace.x) + (binormal * tSpace.y) + (normal * tSpace.z);
return normalize(mul((float3x3)_Object2World, normalO));
}
inline float2 RadialCoords(float3 a_coords)
{
float lon = atan2(a_coords.z, a_coords.x);
float lat = acos(a_coords.y);
float2 sphereCoords = float2(lon, lat) * (1.0 / PI);
return float2(sphereCoords.x * 0.5 + 0.5, 1 - sphereCoords.y);
}
float4 frag(v2f IN) : COLOR
{
// Normalisation.
IN.viewDir = normalize (IN.viewDir);
// Textures.
fixed3 albedo = tex2D(_MainTex, IN.uv).rgb;
float3 normal = UnpackNormal(tex2D(_BumpMap, IN.uv));
float3 specular = tex2D(_SpecularTex, IN.uv).rgb;
// Vectors.
float3 normalW = TangentToWorld ( normal, IN.normal, IN.tangent, IN.binormal);
float2 ambCoords = RadialCoords(normalW);
float3 refl = -reflect(IN.viewDir, normalW);
float2 specCoords = RadialCoords(refl);
// Mip values. Sampling the lowest mip gives a uniform colour, so I'm sampling second lowest mip.
float ambMip = _EquirectangularBlur - 1;
float specMip = (1 - specular.g) * _EquirectangularBlur;
// Ambient.
float3 amb = tex2Dlod(_Equirectangular, float4(ambCoords.xy, 0, ambMip)).rgb;
// Specular.
float3 spec = tex2Dlod(_Equirectangular, float4(specCoords.xy, 0, specMip)).rgb;
// Fresnel.
float VdotN = dot( IN.viewDir, normalW );
float fresnel = pow( abs(1.0 - VdotN), 5.0 );
fresnel += specular.b * ( 1.0 - fresnel );
float specMultiplier = fresnel * specular.r;
// Result.
float4 c;
c.rgb = albedo * amb + spec * specMultiplier;
c.a = 1.0;
return c;
}
ENDCG
}
}
FallBack "VertexLit"
}
@Smart2itDavid
Copy link

Hey,
I'm getting distortions around the north and south pole of my equirectangular image. I made the cubemap by rendering a camera to the cubemap from within an inverted 2x2x2 cube with a checkers texture on it. The image below shows what a skybox looks like when it is using the image imported as a cubemap.
South Pole

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment