This addresses quirks with the lighting engine and how it interacts with custom shaders.
Futile is a bit of a frankenstein's monster when it comes to iteroperability with base Unity systems; while Unity shaders work, the behavior is overshadowed by Futile's layer system. This caused me to spend more hours than I care to admit trying to figure out why an unlit holographic/additive shader I wrote was casting shadows, and I want to prevent that pain for anyone else who may be reading.
This section covers shader information. It is directed towards those that already understand fundamental shader concepts.
- The
ZWrite
andZTest
commands (among less common others) are ignored. Futile does not have a depth buffer.1- Vanilla uses certain declarations that are useless as well (their inclusion is erroneous). These are:
Lighting
(seen asLighting Off
) does nothing: Shaders made with manually declared vertex/fragment programs are never subjected to default lighting (you need to write that in yourself).Fog
(seen asFog { Color(0,0,0,0) }
) does nothing: The vanilla shaders don't declare#pragma multi_compile_fog
so Unity fog can't apply.BindChannels
declaration. This is an artifact of extremely old shader code and bindings are now done in the appdata struct itself.Category
block. This is only really useful if there's more than one subshader with common settings.- Declaring
register(s0)
on the grab texture. This is another artifact of extremely old shader code and is effectively useless in the face of modern HLSL spec.
- Vanilla uses certain declarations that are useless as well (their inclusion is erroneous). These are:
- ALL shader tags are ignored.
- Futile uses the
sortZ
property ofFNode
to order sprite rendering instead, alongside the layer system (seeRoomCamera.ReturnFContainer
); you cannot use theQueue
tag.- FContainers (layers) are rendered sequentially, and each sprite within is rendered based on its sortZ property (higher = later). For more information on what layers are available and in what order, see
RoomCamera..ctor
sortZ
may not apply automatically, seeFContainer.shouldSortByZ
(if false, you will need to callSortByZ()
manually). Any sprites added after this function call will draw on top regardless of their sort value, so ensure this is called after the last sprite is added!sortZ
exists purely within its own container and can not be used to draw on top of or below other containers.
- FContainers (layers) are rendered sequentially, and each sprite within is rendered based on its sortZ property (higher = later). For more information on what layers are available and in what order, see
- Futile uses the
- Black is pseudo-transparency. In Futile, black
(0, 0, 0)
has a special behavior that causes it to grab the pixel from the layer below and copy its color. This creates the illusion of transparency, without actually having a transparent pixel (hence the term "pseudo-transparency"). This is used for various effects, such as Pebbles's halo.- If you need to render black, consider using a value of
0.004
, which is as close to black as you can sensibly get in an 8 bit color setup.
- If you need to render black, consider using a value of
- The stencil buffer works iff it is enabled.1
- The
Cull
command does work, but is effectively useless. - Blend modes do work, and apply to the current state of the entire frame buffer as one might expect (this includes applying on top of lower layers).
- The default (or at least, expected) blend mode for shaders is
Blend SrcAlpha OneMinusSrcAlpha
. - Translucent objects may not work as expected with lighting, see the next section.
- The default (or at least, expected) blend mode for shaders is
- FContainer does have a
sortZ
property as it inherits fromFNode
, but whether or not this is functional has not been tested; it is not used in vanilla and so leveraging it would require setup ahead of time.
1 It is possible to forcefully add a depth and stencil buffer. To do this, you need to modify the depth bits of the main render texture (see FScreen::renderTexture
, set to 16 for depth buffer, 24 for depth and stencil). Whether or not the depth buffer works is unknown, but the stencil buffer does absolutely work. A toolkit I will be releasing SoonTM will include a function to cleanly do this from any mod, and a PR has been submitted to RegionKit so it may be included there as well.
Rain World populates a number of uniforms, as well as other shader properties. This list may not be complete, but is as complete as possible. Some entries are not documented if their purpose is self-explanatory or unknown. Consider searching for strings in DNSpy/etc. when figuring out what these do.
- "The room's size in pixels", or what I will call
roomSize
, indicates the room's size measured in tiles, times 20 (a tile is 20x20 pixels). Thus, this includes out of bounds tiles as well. Effectively, if it can be set in the level editor, it counts here. - "The camera position", or what I will call
camPos
, refers to an interpolated, runtime camera position. This value can change in realtime! - "The camera position as set by the room", or what I will call
fileCamPos
, refers to the literal camera position that was set in the level editor. This value is constant.
-
Shader Keywords are as follows (REMEMBER: To use these, you need to disable shader variant stripping when exporting your assets!)
SNOW_ON
- If it's snowing right now.SNOW_OFF
is declared as well, but the keyword is never disabled. You should not use this. Use SNOW_ON.
HR
- Enters spoiler territory, but if you know you know.Gutter
- If the room has theDirtyWater
effect (notably, this breaks convention in that it is not all caps!)
-
Shader Uniforms are as follows:
uniform float _RAIN
- Global timer, analogous to (but not the same as)_Time.y
. It is affected by timescale. The value isshadersTime / 5.0f
whereshadersTime
hasTime.deltaTime * timescale
added to it every frame.uniform float2 _screenOffset
- This is representative of an offset for displaying pixels. Outside of OpenGL environments, and when the game's render scale is not 1x (which it always is, unless a mod changes that), this value is half of a pixel in normalized space.uniform float2 _screenSize
- Analogous to_ScreenParams.xy
in native Unity HLSL. This value is the current resolution of the displayed screen (this means mods like Sharpener, while they do make the window larger, do not affect this value).uniform float4 _spriteRect
- Used in the room camera. SeeRoomCamera::DrawUpdate
. This value is particularly complicated to compute:- It begins with the interpolated camera position.
- Screen shake is applied based on a random normalized vector, but...
- If the game is not in the Void Sea, this position gets clamped to ±20px relative to the position of the camera declared by the room itself, and...
- If the game is in the Void Sea, there is a constant -528px offset applied to the Y axis, unless the water is upside down (comes down from the ceiling, as seen in HR), from which there is a constant offset of (the room's height in pixels) + 128px on the Y axis.
- The vector is then floored, and has 0.02 subtracted from both components.
xy = floor(xy) - 0.02
- The vector is offset by the camera's
offset
field. - The vector is offset by the camera's
hardLevelGfxOffset
field. - A
Vector4
is constructed to send to the shader, where... xy
is set to(vector - float2(0.5, -0.5) + roomCameraPosition) / _screenSize
zw
is set to(vector - float2(0.5, -0.5) + levelGraphic.sizeXY) / _screenSize
uniform float4 _camInRoomRect
- This is the position of the camera relative to the room. TheVector2
from_spriteRect
is used here as well, and it is computed as:xy = vector / roomSize
zw = _screenSize / roomSize
uniform float2 _WorldCamPos
- This is used in classRoofTopView
and in the Void Sea scene. It is unset otherwise, and thus should not be used for shaders outside of this context.- In the void sea scene, it is computed as
camPos - scene.sceneOrigo + scene.cameraOffset
(camPos
is theVector2
from_spriteRect
again, andsceneOrigo
andcameraOffset
are members of the scene class itself). - In the RoofTopView class, this is used when drawing the floor, and is
camPos - scene.sceneOrigo
.
- In the void sea scene, it is computed as
uniform float2 _PlayerPos
- The player's position normalized to the width and height of the room. (this will be between 0 and 1, a value of out of that range indicates the player has gone out of bounds).uniform float _Grime
- Identical to the grime value as set by the room settings, as a % from 0 to 1.uniform float _WetTerrain
- Identical to the wet terrain value as set by the room settings, as a % from 0 to 1.uniform float _SwarmRoom
- 0f in normal rooms, and 1f in batfly hive rooms (aka swarm rooms)uniform float _fogAmount
- The strength of the fog effect, if present. 0 otherwise.uniform float _windAngle
- Used in the blizzard graphics. This is not used in any blizzard shaders though, which is a bit perplexing.uniform float _windDir
- Same as above, used in the blizzard graphics class, but not implemented in any blizzard shaders.uniform float _windStrength
- Same as aboveuniform float _snowStrength
- Same as aboveuniform float _waterTime
- Same as aboveuniform float _rimFix
- This value is used to slightly offset the displayed level graphics. Normally 0, but 1 when the room effect isAboveCloudsView
orRoofTopView
uniform float _DustWaveProgress
- Represents the intensity of a dust storm, as a value from 0 to 1.uniform float2 _gridOffset
- Used in the superstructure grid vfx (general systems bus), the name should be self explanatory.uniform float _rainDirection
- Used to tilt the rain slightly.uniform float4 _RainSpriteRect
- Used to scale the rain sprite to the room.xy
iscamPos / roomSize
zw
is the game's default resolution (_x768) divided byroomSize
, with a catch for that first blank on the X size:- If Remix is enabled, it uses the screen width as reported by the camera. If remix is not enabled, it uses a constant 1366f.
w
is not affected by this rule and is always768 / roomSize.y
.
uniform float _rainIntensity
- The intensity of the rain overlay, from 0 to 1.uniform float _rainEverywhere
- Determines if rain should be drawn over the entire screen, or just in areas that are not under something, as a value from 0 to 1.uniform float4 _AboveCloudsAtmosphereColor
- Used inRoofTopView
andAboveCloudsView
to draw the sky.uniform float4 _MultiplyColor
- This is provided in the same context as the cloud color above. This is used when transitioning into or out of dusk.uniform float _waterDepth
- Used to determine where to draw room water, where 0 is in the foreground (over everything) and 1 is in the background (behind everything).uniform float _waterLevel
- The height of the water in the room, in pixels.uniform float4 _lightDirAndPixelSize
- Computed asfloat4(room.lightAngle.xy, 1f / 1400f, 1f / 800f)
. The "pixel size" is always that constant. This constant is used in rooms as well.uniform float4 _MapCol
- Used to color the map overlay in the HUD. This is only set upon loading the game once, and it is uncertain what changing it during runtime will do (though it will probably do what you expect it to do).uniform float4 _MapWaterCol
- The color of water in the map, again only set upon loading the game once.uniform float2 _mapPan
- The offset of the map as the user moves through it, measured in pixels.uniform float2 _mapSize
- The size of the map texture, oddly computed asfloat2(_mapTexture.width, _mapTexture.height / 3);
uniform float4 _transitionColor
- Set in theRoomTransition
class, this is a faded color based on the time between frames presumably when transitioning between rooms.uniform float _BlurDepth
- Computed as1 + focus * 9
wherefocus
is a member ofMenuScene
, and is interpolated.uniform float _BlurRange
- Interpolated betweenblurMin
andblurMax
ofMenuScene
. The interpolation factor issaturate(pow(max(0, focus), 0.75))
.uniform float2 _MenuCamPos
- Used for the fake depth effect in the main menu.uniform float4 _tileCorrection
- Used in the context of blizzards and dust.x
is_screenSize.x / roomSize.x * (1366 / _screenSize.x) * 1.02f
y
is_screenSize.y / roomSize.y * 1.04f
zw
isfileCamPos / roomSize
uniform float4 _EnergyCellCoreCol
- Used inEnergyCell
this is just the color of the center of the cell. This changes based on how long has been used for.uniform float _hologramThreshold
- This value is 0.5 if the render scale is larger than 1x, and 0.65 otherwise.uniform float2 _SceneOrigoPosition
- Used inRoofTopView
, this provides the scene'ssceneOrigo
property to shaders.uniform float4 _LeviathanColorA
- Used inBigEelGraphics
, likely an artifact of older code as this type of property is not seen in other creatures (which instead just directly set the color of the sprites).uniform float4 _LeviathanColorB
- Same as above.uniform float4 _LeviathanColorHead
- Same as above.sampler2D _LevelTex
- The raw image of the level itself, the one rendered by the editor app.sampler2D _PalTex
- The current color palette's texture.sampler2D _mapFogTexture
- Used to hide the map when first opening it, causes that fade in effect where it grows as you remember more of it.sampler2D _WindTex
- Used when generating the wind map of a blizzard. It is a render texture that is written to by a shader to store data.sampler2D _WindTexRendered
- The same as_WindTex
but its variable name refers to it as "interpolated".sampler2D _Original
- Used in the context of classFlowMap
sampler2D _DustFlowTex
- Not investigated, presumably used similarly to the wind texture for blizzards.sampler2D _NoiseTex
-palettes/noise.png
, A smooth perlin noise texture, 64x64sampler2D _NoiseTex2
-palettes/noise2.png
, A pseudorandom RGB noise texture (each component is a random value), 256x256sampler2D _CloudsTex
-illustrations/cloudstexture.png
sampler2D _TextGradientTex
-palettes/textgradient.png
sampler2D _ApartmentsTex
-illustrations/apartments.png
sampler2D _CityPalette
-palettes/citypalette.png
sampler2D _UniNoise
-illustrations/uninoise.png
, appears to be_NoiseTex2
but slightly darker. Perhaps normalized.sampler2D _EnergySwirl
-illustrations/energyring.png
sampler2D _pAngle
-illustrations/pangle.png
See FShader::CreateShader
, which accepts a Unity Shader asset. As you might expect, you can export it via an asset bundle and then load it during runtime. Tutorials for how to do this exist elsewhere and are out of the scope of this post.
The only reason this section is here is to remind you: DO NOT load your shaders in your BepInEx Plugin's Awake
! You will hard-crash Unity on startup when calling LoadAsset
. Hook into RainWorld.LoadResources
and load your asset bundle there instead.
In general, you should have some sort of loader class that statically stores your shaders. Do NOT call CreateShader
repeatedly, and do NOT set the shader every frame! This will cause lag and is completely the wrong way to do it. Create your objects once and reuse them.
An example "loader class" might be as follows:
namespace MyMod {
internal class Assets {
// This references your shader after the game loads assets. Use this to get your shader.
internal static FShader MyCustomShader { get; private set; }
internal static void Initialize() {
// Call this from your mod's OnEnable/Awake
On.RainWorld.LoadResources += (originalMethod, @this) => {
AssetBundle myAssets = ...; // This is up to you to write. See https://stackoverflow.com/questions/3314140/how-to-read-embedded-resource-text-file - the same applies for loading literally any type of file, not just text.
Shader myShader = myAssets.LoadAsset<Shader>("Assets/myshader.shader"); // This is the same path as the one in your Unity editor.
MyCustomShader = FShader.CreateShader(myShader.name, myShader);
};
}
}
}
Then, to use this shader, simply edit the shader
property of any FSprite
.
Sometimes, you might need to change your own shader's properties. To do this, you need to access the actual Unity material itself that backs the FSprite
. This is thankfully very easy.
private FSprite _myApplicableSprite; // Set this in your InitiateSprites method.
// This object is a Unity instance that stores per-instance material properties. It is significantly faster than
// setting a Renderer's material property (such that it doesn't share its material and renders separately).
// Note that it is static! **DO NOT** create a new MaterialPropertyBlock every time Draw/Update/etc gets called!
// If you make a new one on demand like that, you can (and probably will) lag the ever living hell out of the game.
// MaterialPropertyBlocks are designed to be cached and shared, it's the entire purpose of their existence.
private static MaterialPropertyBlock _materialProps = new MaterialPropertyBlock();
override void Draw(float timeStacker) {
Renderer renderer = _myApplicableSprite._renderLayer._meshRenderer;
renderer.GetPropertyBlock(_materialProps); // This loads existing data into the property block, so that it matches what you expect it to be on this renderer.
_props.SetFloat("_YourProperty", ...); // This sets the property for this draw call. There are other Set methods (SetColor, SetVector, SetTexture, ...)
renderer.SetPropertyBlock(_materialProps); // This updates the data for rendering.
}
This section covers quirks with the lighting system.
- Lights will cast a shadow on all objects that are not 100% transparent, regardless of shader blend mode. This means that translucent (partly transparent) objects and will still cast shadows if a light is above them. This can be avoided by putting the object on a layer higher or equal to that of the light, or by putting the light's layer below it. Both of these solutions are less than ideal.
- In general, you will want to avoid translucent objects where lighting is also present, as it is quite a hassle to ensure things render in the right order.
- Flat lights do not cast shadows and are not subject to this problem.
- For the sake of completeness, it is worth it to mention that shadows are not computed traditionally in Rain World; the depth buffer has no bearing on shadows, and there is no such thing as a shadowcaster pass. If it is a pixel that is on the screen and beneath a light, it casts a shadow, no exceptions.
- Lights will cast shadows on all objects present in layers beneath them, but not on the layer they are currently on or higher layers.
- Changing a
LightSource
's layer will change where shadows are drawn, however this can have unexpected behaviors, and belowBackground
this will break the renderer with some rather unpleasant flashing. Layers higher than water have not been tested by me. - Changing the
sortZ
of aLightSource
s sprite(s) will change when it draws, however this behavior is broken due to how lighting works (see below).
- Changing a
In this scenario, the LightSource
's sprites are on sortZ=9999, and the puppet on 0. As you can see, the shadow computes its depth onto the world behind the puppet, then the puppet gets drawn, then the shadow gets drawn as if it's not actually there. Despite being opaque, the puppet is not casting a proper shadow, and is receiving the shadows for the world behind it instead of for itself.