Table of Contents

Textures

A texture is a material property: declare a property of a texture type, and it appears as a texture slot in the Material Inspector. In shader code, the property is the texture itself, with methods to sample it.

[MainTexture] public Texture2D BaseMap { get; } = Texture2D.White;

public override void Frag(FragmentInput IN, ref LitFragmentOutput OUT)
{
    float2 uv = BaseMap.GetTransformedUV(IN.uv0);
    OUT.albedo = (half3)BaseMap.Sample(uv).rgb;
}
Type What it is Sampled with
Texture2D An ordinary 2D texture. float2 UVs
Texture2DArray A stack of 2D textures of the same size, selected by index. float3: UVs and the slice index
Texture3D A volume of texels. float3 coordinates from 0 to 1
Cubemap Six square textures covering every direction, for skies and reflections. a float3 direction

Each texture is sampled with the filtering and wrapping set in its import settings.

Sampling

Method What it does Where it works
Sample(uv) Samples with filtering, choosing the mipmap automatically. Frag
SampleLevel(uv, lod) Samples a specific mipmap level: 0 is the full resolution. everywhere
SampleBias(uv, bias) Like Sample, with the mipmap shifted: positive values blur, negative values sharpen. Frag
SampleGrad(uv, dpdx, dpdy) Chooses the mipmap from UV derivatives that you provide. everywhere
Load(pixel) Reads a single texel, without filtering. everywhere

Sample chooses the mipmap from how fast the UVs change between neighboring pixels, which only exists in Frag. In Vert, for example to displace vertices by a height map, use SampleLevel:

public override void Vert(VertexInput IN, ref VertexOutput OUT)
{
    float2 uv = IN.uv0;
    float height = HeightMap.SampleLevel(uv, 0).r;
    OUT.positionOS += IN.normalOS * height * Amplitude;
}

Tiling and Offset

A Texture2D slot has Tiling and Offset fields in the Material Inspector. They aren't applied automatically: pass the UVs through GetTransformedUV, which computes uv * tiling + offset, before sampling. GetTiling returns the tiling alone.

For textures whose tiling the material shouldn't control, for example masks that share the UVs of another texture, add [NoScaleOffset]: it hides the fields, and GetTransformedUV returns the UVs unchanged.

Default textures

A Texture2D property's initial value is the texture used while no texture is assigned: Texture2D.White, Black, Gray, LinearGray, Red, or Bump, a flat normal map. Choose the value that leaves the surface unchanged: white for a color map that multiplies the base color, Bump for a normal map, LinearGray for a detail map that multiplies by 2.

To skip sampling a texture that isn't assigned at all, see [TextureIsSet] in Material properties.

Normal maps

A normal map stores directions relative to the surface, in tangent space. To use it, decode the sample and convert the result to world space:

[Normal] public Texture2D NormalMap { get; } = Texture2D.Bump;
[Range(0, 2)] public float NormalStrength { get; } = 1;

public override void Frag(FragmentInput IN, ref LitFragmentOutput OUT)
{
    float2 uv = NormalMap.GetTransformedUV(IN.uv0);
    float3 normalTS = ShaderApi.UnpackNormalScale(NormalMap.Sample(uv), NormalStrength);

    float3x3 tangentToWorld = ShaderApi.CreateTangentToWorld(IN.normalWS, IN.tangentWS.xyz, IN.tangentWS.w);
    OUT.normalWS = ShaderApi.TransformTangentToWorld(normalTS, tangentToWorld, true);
}

[Normal] makes the Material Inspector warn, with a fix button, when the assigned texture isn't imported as a normal map. ShaderApi.BlendNormalRNM combines two tangent-space normals, for example a detail normal map over a base one.

Triplanar mapping

Triplanar mapping projects a texture from three directions, along the world X, Y and Z axes, and blends the projections by the surface normal. It needs no UVs and avoids stretching on rocks, terrain and other geometry without good UVs, at the cost of three samples instead of one.

// Repeats twice per world unit, with sharp transitions between projections.
float4 color = BaseMap.SampleTriplanar(IN.positionWS, IN.normalWS, 2, 4);

The third argument, scale, is how many times the texture repeats per world unit, and blend how sharp the transitions between projections are. The projection stays fixed in the world on both render pipelines, and the texture's Tiling and Offset still apply.

  • To project in object space, so that the texture moves with the object, use SampleTriplanarAt with an object-space position and normal.
  • SampleTriplanarNormal and its variants do the same for normal maps, and return a tangent-space normal.
  • The Level variants take a mipmap level and work in Vert.

Parallax occlusion mapping

Parallax occlusion mapping makes a flat surface look deeply relieved: it traces the view ray through a height map to find the point the viewer actually sees, and returns a UV offset to apply to all the surface's textures.

float3 viewDirWS = ShaderApi.GetWorldSpaceNormalizeViewDir(IN.positionWS);
float3 viewDirTS = ShaderApi.GetViewDirectionTangentSpace(IN.tangentWS, IN.normalWS, viewDirWS);

float2 uv = BaseMap.GetTransformedUV(IN.uv0);
float height;
// Between 8 and 32 steps depending on the view angle; fades out above mipmap level 4.
float2 offset = HeightMap.SampleParallaxOcclusion(uv, viewDirTS, Depth * BaseMap.GetTiling(), 8, 32, 4, out height);
uv += offset;

The height is read from the red channel, 1 being the top of the relief. For a cheaper, flatter effect, ShaderApi.ParallaxOffset1Step computes a single-step offset.

Arrays, 3D textures and cubemaps

  • Texture2DArray holds several same-sized textures, for example the layers of a terrain or the frames of an animation. Sample(new float3(uv, index)) samples slice index.
  • Texture3D is a volume, for 3D noise or color grading lookup tables. Sample it with coordinates from 0 to 1 on each axis.
  • Cubemap covers every direction, for skies and custom reflections. Sample it with a direction, which doesn't need to be normalized: EnvMap.Sample(math.reflect(-viewDirWS, normalWS)).

These types can't have an initial value, and their UVs aren't affected by Tiling and Offset.

The screen

Every shader can read the scene behind the object: what the opaque objects rendered, and how far away they are. That's what water, glass, heat haze and soft particles need. Sample it at the pixel's own screen position:

float2 screenUV = ShaderApi.GetNormalizedScreenSpaceUV(IN.positionCS.xy);
float3 behind = ShaderApi.SampleSceneColor(screenUV);

// How much is between this surface and the scene behind it, in world units.
float sceneDistance = ShaderApi.LinearEyeDepth(ShaderApi.SampleSceneDepth(screenUV));
float pixelDistance = ShaderApi.LinearEyeDepth(IN.positionCS.z);
float thickness = sceneDistance - pixelDistance;

Offset screenUV, for example by the normal map, to bend what's behind, as refraction does.

The render pipeline copies the color and the depth after the opaque objects and before the transparent ones, so only transparent materials see the current frame. Also:

  • URP: enable Opaque Texture and Depth Texture, on the camera (Rendering) or on the URP asset. The opaque texture is downsampled to half resolution by default: set Opaque Downsampling on the URP asset to None for a sharp image.
  • HDRP: both are always available to transparent materials.

The color is what the frame holds, so OUT.albedo of an Unlit shader shows it unchanged on both pipelines. HDRP applies the camera's exposure to emission, so to show it through OUT.emission, for example on top of a Lit surface, write ShaderLib.ApplyExposureWeight(color, 0): on URP it changes nothing.

Fullscreen shaders read the image differently, at the point where their pass runs. See Fullscreen shaders.