Hello!
So I’m working on re-producing the atmosphere shader done by Sebastian Lague (
) in URP instead of HDRP. I’ve got a Blit-pass built and working for applying a material to a full-screen quad, copying back and forth between the camera buffer, etc. , have depth-textures turned on for my camera, and things are basically working. I’ve been beating my head against the wall for a few days now and just can’t figure out what’s going on. Here is a screenshot of the effect that I’m getting at the moment:
As you can see, I’m getting the good volume rendering with the light scattering and the whole bit, but the direction from the atmospheric sampling points to the light source (that yellow “sun” in the background) is wonky as hell. BTW, this is a donut that surrounds the planet regardless of the direction that you view it from; in other words, it’s in world-space like that, not just an artifact of the camera viewing direction.
Here is my shader code:
Shader "Unlit/AtmosphereVolume"
{
Properties
{
_MainTex("Texture", 2D) = "white" {}
_NumScatteringInPoints("Number of scattering points", Int) = 10
_NumOpticalDepthPoints("Number of optical depth sample points", Int) = 10
[HideInInspector] _PlanetCenter("Planet center", Vector) = (0,0,0) // World-space
[HideInInspector] _PlanetRadius("Planet radius", Float) = 1
[HideInInspector] _AtmosphereRadius("Atmosphere radius", Float) = 1
[HideInInspector] _DensityFalloff("Atmosphere density falloff", Float) = 1
[HideInInspector] _DirToSun("Direction towards the sun", Vector) = (0,0,0) // Direction from planet to the sun
[HideInInspector] _ScatteringCoefficients("Scattering wavelengths", Vector) = (0,0,0)
}
SubShader
{
// No culling or depth.
Cull Off ZWrite Off ZTest Always
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#define FLT_MAX 3.402823466e+38
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float3 viewVector : TEXCOORD1;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _CameraDepthTexture;
float3 _PlanetCenter;
float _AtmosphereRadius;
float _PlanetRadius;
float _DensityFalloff;
int _NumScatteringInPoints;
int _NumOpticalDepthPoints;
float3 _DirToSun;
float3 _ScatteringCoefficients;
float2 raySphere(float3 sphereCentre, float sphereRadius, float3 rayOrigin, float3 rayDir) {
float3 offset = rayOrigin - sphereCentre;
float a = 1;// dot(rayDir, rayDir);// if rayDir might not be normalized
float b = 2 * dot(offset, rayDir);
float c = dot(offset, offset) - sphereRadius * sphereRadius;
float d = b * b - 4 * a * c; // Discriminant from quadratic formula
// Number of intersections: 0 when d < 0; 1 when d = 0; 2 when d > 0
if (d > 0) {
float s = sqrt(d);
float dstToSphereNear = max(0, (-b - s) / (2 * a));
float dstToSphereFar = (-b + s) / (2 * a);
// Ignore intersections that occur behind the ray
if (dstToSphereFar >= 0) {
return float2(dstToSphereNear, dstToSphereFar - dstToSphereNear);
}
}
// Ray did not intersect sphere
return float2(FLT_MAX, 0);
}
float densityAtPoint(float3 densitySamplePoint) {
float heightAboveSurface = length(densitySamplePoint - _PlanetCenter) - _PlanetRadius;
float height01 = heightAboveSurface / (_AtmosphereRadius - _PlanetRadius);
float localDensity = exp(-height01 * _DensityFalloff) * (1 - height01);
return localDensity;
}
float opticalDepth(float3 rayOrigin, float3 rayDir, float rayLength) {
float3 densitySamplePoint = rayOrigin;
float stepSize = rayLength / (_NumOpticalDepthPoints - 1);
float opticalDepth = 0;
for (int i = 0; i < _NumOpticalDepthPoints; i++) {
float localDensity = densityAtPoint(densitySamplePoint);
opticalDepth += localDensity * stepSize;
densitySamplePoint += rayDir * stepSize;
}
return opticalDepth;
}
float3 calculateLight(float3 rayOrigin, float3 rayDir, float rayLength, float3 originalColor) {
float inScatterPoint = rayOrigin;
float stepSize = rayLength / (_NumScatteringInPoints - 1);
float3 inScatteredLight = 0;
float viewRayOpticalDepth = 0;
for (int i = 0; i < _NumScatteringInPoints; i++) {
float sunRayLength = raySphere(_PlanetCenter, _AtmosphereRadius, inScatterPoint, _DirToSun).y;
float sunRayOpticalDepth = opticalDepth(inScatterPoint, _DirToSun, sunRayLength);
viewRayOpticalDepth = opticalDepth(inScatterPoint, -rayDir, stepSize * i);
float3 transmittance = exp(-(sunRayOpticalDepth + viewRayOpticalDepth) * _ScatteringCoefficients);
float localDensity = densityAtPoint(inScatterPoint);
inScatteredLight += localDensity * transmittance * _ScatteringCoefficients * stepSize;
inScatterPoint += rayDir * stepSize;
}
float originalColorTransmittance = exp(-viewRayOpticalDepth);
return originalColor * originalColorTransmittance + inScatteredLight;
}
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
// Camera space matches OpenGL convention where forward is -z. In unity forward is +z.
float3 viewVector = mul(unity_CameraInvProjection, float4(v.uv.xy * 2 - 1, 0, -1));
o.viewVector = mul(unity_CameraToWorld, float4(viewVector, 0));
return o;
}
float4 frag (v2f i) : SV_Target
{
float4 originalColor = tex2D(_MainTex, i.uv);
float sceneDepthNonLinear = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
float sceneDepth = LinearEyeDepth(sceneDepthNonLinear) * length(i.viewVector);
float3 rayOrigin = _WorldSpaceCameraPos;
float3 rayDir = normalize(i.viewVector);
float2 hitInfo = raySphere(_PlanetCenter, _AtmosphereRadius, rayOrigin, rayDir);
float dstToAtmosphere = hitInfo.x;
float dstThroughAtmosphere = min(hitInfo.y, sceneDepth - dstToAtmosphere);
if (dstThroughAtmosphere > 0) {
const float epsilon = 0.0001;
float3 pointInAtmosphere = rayOrigin + rayDir * (dstToAtmosphere + epsilon);
float3 light = calculateLight(pointInAtmosphere, rayDir, dstThroughAtmosphere - epsilon * 2, originalColor);
return float4(light, 1);
}
return originalColor;
}
ENDCG
}
}
}
You can ignore all the [HideInInspector] crap at the top - I was using the material view for my inputs when I was first working on things, now I have a simple script to set the values:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Atmosphere : MonoBehaviour
{
public Material atmosphereMaterial;
public Transform sun;
public Transform planet;
public float planetRadius = 1f;
public Vector3 waveLengths = new Vector3(700, 530, 440);
public float scatteringStrength = 1f;
public float densityFalloff = 2f;
[Range(1, 10)]
public float atmosphereScale = 1;
public void Start()
{
SetScatterings();
}
void Update()
{
SetPositions();
}
private void OnValidate()
{
SetScatterings();
SetPositions();
}
void SetScatterings()
{
float scatterR = Mathf.Pow(400 / waveLengths.x, 4) * scatteringStrength;
float scatterG = Mathf.Pow(400 / waveLengths.y, 4) * scatteringStrength;
float scatterB = Mathf.Pow(400 / waveLengths.z, 4) * scatteringStrength;
Vector3 scatteringCoefficients = new Vector3(scatterR, scatterG, scatterB);
atmosphereMaterial.SetVector("_ScatteringCoefficients", scatteringCoefficients);
atmosphereMaterial.SetFloat("_DensityFalloff", densityFalloff);
}
void SetPositions()
{
Vector3 directionToLight = (sun.position - planet.position).normalized;
atmosphereMaterial.SetVector("_DirToSun", directionToLight);
atmosphereMaterial.SetVector("_PlanetCenter", planet.position);
float atmosphereSize = planetRadius * (1 + atmosphereScale) - planetRadius;
atmosphereMaterial.SetFloat("_PlanetRadius", planetRadius);
atmosphereMaterial.SetFloat("_AtmosphereRadius", atmosphereSize);
}
}
So yeah… That’s where I’m stuck. I’m hoping there is something about URP that isn’t well documented, or at least that I don’t know about, that will make this “click” for me, but so far I’m pretty dang confused as to why this is happening. Any help is much appreciated!


