Alain Galvan ·3/18/2017 5:30 AM · Updated 1 year ago
A method of rendering starry skies in real time that doubles as indirect lighting for physically based rendering.
Tags: researchphysicallybasedrenderingpaper3dui2017githubunreal
![]()
For the Realtime Celestial Rendering Paper, I made a dynamic, responsive stary sky that the player would see while exploring the star system. To do this, I wrote a some shaders that renders onto a cubemap texture target, which could then be used as a simple skybox or as an animated radiance / irradiance cubemap for Physically Based Rendering.

To make this more manageable and portable, the shader was written in HLSL rather than using Unreal's Material compositor tools.

Not that it isn't possible with the custom node in Unreal's Material Editor, but C++ gives you more programmatic freedom. Another direction could have been to use the option to convert Unreal materials to HLSL code.
Step By Step Process:
new YourShaderPlugin(params) object.
A Cubemap Texture is a 6 sided texture that allows for very low cost sampling of an environmental texture that can serve as a skybox or mapped onto any object with normals, or mapped onto a cube. This means this texture could potentially be used as a light source for PBR calculations, so it's a good idea to make it an HDR texture for skyboxes.
There's several ways to map a cubemap texture, linearly, boxlike, panoramic (what unreal uses), having 6 separate textures. The standard is currently native cubemaps since they're fast, can easily be used as light sources or reflection sources.
The Render Hardware Interface (RHI) is Unreal's rendering abstraction layer that communicates with OpenGL or DirectX, and this, combined with a cross compiler for HLSL code, let's you write platform independent shaders. It's from here and related systems like the RenderCore, Renderer, and ShaderCore where we'll be getting our data structures/macros. Here's a few of the headers involved with the RHI.
├─ UnrealEngine/Engine/Source/Runtime/RHI/
│ ├─ RHICommandList.h # Manages Rendering Queue
│ └─ RHIResources.h # Stores Abstract Graphics Data Structures
└─ UnrealEngine/Engine/Source/Runtime/RenderCore/Public/
└─ RenderingThread.h # Rendering requests are Enqueued HereThe RHI manages a rendering queue, handles all render requests. You need to make requests to the RHI to render onto your render target.
The key to raymarching a cubemap is to build a camera matrix to orient rays around, and pass that as a uniform.
The cubemap is then composed from 6 render target shader executions:
//Camera Matrix Struct
struct FLocal
{
/** Creates a transformation for a cubemap face, following the D3D cubemap layout. */
static FMatrix CalcCubeFaceTransform(ECubeFace Face)
{
static const FVector XAxis(1.f, 0.f, 0.f);
static const FVector YAxis(0.f, 1.f, 0.f);
static const FVector ZAxis(0.f, 0.f, 1.f);
// vectors we will need for our basis
FVector vUp(YAxis);
FVector vDir;
switch (Face)
{
case CubeFace_PosX:
vDir = XAxis;
break;
case CubeFace_NegX:
vDir = -XAxis;
break;
case CubeFace_PosY:
//Reversed PosY and -Y...
vUp = ZAxis;
vDir = -YAxis;
break;
case CubeFace_NegY:
vUp = -ZAxis;
vDir = YAxis;
break;
case CubeFace_PosZ:
vDir = ZAxis;
break;
case CubeFace_NegZ:
vDir = -ZAxis;
break;
}
// derive right vector by cross product
FVector vRight(vUp ^ vDir);
// create matrix from the 3 axes
return FBasisVectorMatrix(vRight, vUp, vDir, FVector::ZeroVector);
}
};
for (int32 faceidx = 0; faceidx < (int32)ECubeFace::CubeFace_MAX; faceidx++)
{
const ECubeFace TargetFace = (ECubeFace)faceidx;
const FMatrix ViewRotationMatrix = FLocal::CalcCubeFaceTransform(TargetFace);
ENQUEUE_UNIQUE_RENDER_COMMAND_THREEPARAMETER(
FPixelShaderRunner,
FCubeRenderTargetShader*, PixelShader, this,
ECubeFace, TargetFace, TargetFace,
FMatrix, ViewRotationMatrix, ViewRotationMatrix,
{
PixelShader->RunShaderInternal(FResolveParams(FResolveRect(), TargetFace), ViewRotationMatrix);
});
}Then to raymarch it's simply a matter of multiplying the calculated view matrix by a vector built from the UV coordinates of the Input Geometry.
void MainPixelShader(
in float2 UV : TEXCOORD0,
out float4 OutColor : SV_Target0
)
{
float time = PSVariables.TotalTimeElapsedSeconds * 0.01;
float4 rayDirection = normalize(mul(PSVariables.ViewMatrix, float4(UV - float2(0.5, 0.5), .5, 0.)));
OutColor.rgb = raymarch(rayDirection, time);
OutColor.a = 1.0;
}To make managing custom shaders easier, it's a good idea to create a plugin for your Unreal Engine build that features the shader you're making. In this plugin you can put all the logic related to your shader. In our project, I made a plugin called CubeRenderTargetShader, a plugin that gives you the ability to write a shader built into the plugin onto a cubemap.
Here's the file structure of the plugin:
├─ Public/ │ ├─ CubeRenderTargetShader.h │ ├─ PixelShaderDeclaration.h │ └─ PixelShaderPrivatePCH.h └─ Private/ ├─ CubeRenderTargetShader.cpp └─ PixelShaderDeclaration.cpp
A shader has two interfaces that it can communicate with:
the TEXT("string") denotes the name of the uniform. This will be an automatically generated struct you can refer to. Note this needs to be unique to the shader since they can be accessed from any shader.
//In your .h
BEGIN_UNIFORM_BUFFER_STRUCT(FPixelShaderConstantParameters, )
DECLARE_UNIFORM_BUFFER_STRUCT_MEMBER(float, SimulationSpeed)
END_UNIFORM_BUFFER_STRUCT(FPixelShaderConstantParameters)
BEGIN_UNIFORM_BUFFER_STRUCT(FPixelShaderVariableParameters, )
DECLARE_UNIFORM_BUFFER_STRUCT_MEMBER(FMatrix, ViewMatrix)
DECLARE_UNIFORM_BUFFER_STRUCT_MEMBER(float, TotalTimeElapsedSeconds)
END_UNIFORM_BUFFER_STRUCT(FPixelShaderVariableParameters)ers);
//In your .cpp
IMPLEMENT_UNIFORM_BUFFER_STRUCT(FPixelShaderConstantParameters, TEXT("PSConstants"));
IMPLEMENT_UNIFORM_BUFFER_STRUCT(FPixelShaderVariableParameters, TEXT("PSVariables"));
//Now let's implement the shader!
IMPLEMENT_SHADER_TYPE(, FVertexShaderExample, TEXT("PixelShaderExample"), TEXT("MainVertexShader"), SF_Vertex);
IMPLEMENT_SHADER_TYPE(, FPixelShaderDeclaration, TEXT("PixelShaderExample"), TEXT("MainPixelShader"), SF_Pixel);Now you have everything you need to write your shader! Your shader should be written in as a .usf file, and located in ${ProjectDir}/Shaders. Now making complex shaders isn't too hard, for more details on the subject visit ShaderToy. There's a lot of complex shaders done in GLSL, which shouldn't be too hard to port if you look into the MSDN HLSL Reference. Here's an example of a very simple shader that draws a texture.

To run the shader in game, it was a simple matter of making an actor that executes the command to run the shader and provides the shader with a output target.
void ASpaceSky::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
//Run
if (NULL != PixelShading)
{
PixelShading->RunShader(RenderTargetCube, InputTexture);
}
}