Recently I tried implementing a fake depth parallax shader. The basic idea I had is to make it look like the texture given is some depth $D$ units below the surface. The surface itself could vary in refractive index, maybe to make it look like it’s encased in glass or resin.

First I started by drawing a diagram like this,

What we’re looking for is that little offset $\mathbf{u}$.

Since we’re working in terms of UV coordinates on the surface, it’s helpful to think in terms of tangent space, so we have to transform our view direction by the inverse of the TBN matrix. As it’s orthonormal, its inverse is its transpose.

\[\mathbf{v} = \text{TBN}^\text{T} \cdot \text{worldspace view direction}\]

Please make sure that when generating your TBN matrix, you’re using all three components generated by your model importer of choice, not just computing the cross product of the first two. See here for details.

Working in tangent space means that $\mathbf{n} = (0, 0, 1)$, so $(\mathbf{n} \cdot \mathbf{v}) = v_3 = \cos \theta_o$. Now $\mathbf{u}$ can be found by doing some basic trigonometry,

\[\mathbf{u} = -\frac{D}{v_3} \cdot \mathbf{v}_{12}\]

If we want to do refraction it’s as simple as refracting the view direction and then using that in the final $\mathbf{u}$ calculation,

\[\mathbf{v}' = \text{refract}(\mathbf{v}, \mathbf{n}, \eta)\]

So our parallax code looks like,

float2 subsurfaceParallax(float2 uv, float3 viewDir, float eta, float depth)
{
	float2 refractedViewDir = refract(-viewDir, float3(0.0, 0.0, 1.0), eta);
	float2 u = depth / refractedViewDir.z * refractedViewDir.xy;
	return uv - u;
}

We can then use this to sample our texture with the new UV coordinates as usual.