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
SampleTriplanarAtwith an object-space position and normal. SampleTriplanarNormaland its variants do the same for normal maps, and return a tangent-space normal.- The
Levelvariants take a mipmap level and work inVert.
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
Texture2DArrayholds several same-sized textures, for example the layers of a terrain or the frames of an animation.Sample(new float3(uv, index))samples sliceindex.Texture3Dis a volume, for 3D noise or color grading lookup tables. Sample it with coordinates from 0 to 1 on each axis.Cubemapcovers 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.