//Pixel Art Parallax (Grid Traversal) by Ville Mönkkönen (https://instantkingdom.com/) float4 PixelArtParallax(out float worldZ, float3 tangentViewDir, sampler2D _HeightMap, float4 uvstart, float2 antialiasedUV, float4 _MainTex_TexelSize, float3 worldPos, float3 SGWorldSpaceCameraPos, float4 uvLimits) { /* This is my own idea, it's a parallax mapping system that goes through individual pixels one by one. Since it goes through all the pixels in an image, it's fairly slow for huge textures. But for pixel art it's perfect, since this method preserves the hard pixel edges. Algorithm: 1: Find point where the camera ray enters the sprite, this is plain UV in tangent space. This is supplied as parameter uvstart. 2. Find point where the camera ray exits the sprite, this is along the ray in some specified depth along the z axis. Depth needs to be specified in pixels. 3: Traverse all 2d pixels along the sprite, noting the z point where the ray enters the pixel 4: If the sprite height at the point on the sprite is higher than the ray z at that point, we've found the pixel we hit. */ worldZ = worldPos.z; float thicknessPixels = uvstart.z; float depthToStart = uvstart.w; //in percent, how deep we are in the parallax float depthToStartInv = 1 - depthToStart; float searchDepthPixels = -thicknessPixels * depthToStartInv; //this is how many pixels deep our sprites can be, the depthToStart factor tells how deep in the parallax the vertex is float2 searchDepthUV = _MainTex_TexelSize.xy * searchDepthPixels; float2 uvend = uvstart.xy - tangentViewDir.xy * (searchDepthUV / tangentViewDir.z); //traverse 2d grid, from https://www.shadertoy.com/view/XddcWn { float pixelStride = 1;//how big the pixels are, we can use this to skip every other pixel for example _MainTex_TexelSize.zw /= pixelStride; _MainTex_TexelSize.xy *= pixelStride; float2 p0 = uvstart.xy * _MainTex_TexelSize.zw; float2 p1 = uvend.xy * _MainTex_TexelSize.zw; float2 rd = (p1 - p0); int2 stp = int2(rd.x >= 0.0 ? 1.0 : -1.0, rd.y >= 0.0 ? 1.0 : -1.0); //this is the direction to travel in each direction float2 p = floor(p0); float2 safeRd = sign(rd) * max(abs(rd), float2(1e-6, 1e-6)); float2 rdinv = 1.0 / safeRd; float2 delta = min(rdinv * stp, 1.0); //this is the distance to travel in each direction as percents // start at intersection of ray with initial cell float2 t_max = ((p + max(stp, float2(0.0, 0.0)) - p0) * rdinv); //this is the percent of ray traveled in x and y directions float HeightEnteringPixel = 1; float next_t = 0; //this is the percent of the line of next intersection with the grid, it goes from 0 to 1 as we enter new pixels float2 uvToTest; //middle of the pixel we're currently testing int testCount = 1; int horizontalOrVertical = 0;//if you need to know whether the pixel we hit was horizontal or vertical, we keep track for (int i = 0; i < 64; ++i) { next_t = min(t_max.x, t_max.y); //Normalized depth along the ray, expressed in “height space” (0 to 1) float HeightLeavingPixel = max(0, 1 - next_t); //test begins at the top of the test depth, and ends at 0 uvToTest = (p + float2(0.5, 0.5)) * _MainTex_TexelSize.xy; uvToTest = clamp(uvToTest, uvLimits.xy, uvLimits.zw); float4 depthSampled = tex2DFlip(_HeightMap, uvToTest); float4 depthSampledOffset = depthSampled / (1 - depthToStart); //offset by starting depth testCount++; //The sampled value is in the range 0 to 1. Value of 1 is right at the top of the drawn texel plane, with 0 at the bottom. float sampledPixelTop = depthSampledOffset.x; float sampledPixelBottom = depthSampledOffset.y; #ifndef DiscreteTexel { if (HeightEnteringPixel >= sampledPixelBottom && HeightLeavingPixel <= sampledPixelTop) { if (sampledPixelTop - sampledPixelBottom < 0.01) return float4(0, 0, testCount, 0); // Parametric hit position inside this pixel segment //Solve where inside that segment the height intersects float tHit = (HeightEnteringPixel - sampledPixelTop) / (HeightEnteringPixel - HeightLeavingPixel); //At what point along the ray segment does the ray intersect the top plane of the pixel’s solid volume? tHit = saturate(tHit); // Ray entry and exit positions in UV space //Interpolate the UV along the ray float2 uvEnter = (p0 + (1.0 - HeightEnteringPixel) * rd) * _MainTex_TexelSize.xy; float2 uvExit = (p0 + (1.0 - HeightLeavingPixel) * rd) * _MainTex_TexelSize.xy; float2 uvHit = lerp(uvEnter, uvExit, tHit); uvHit = clamp(uvHit, uvLimits.xy, uvLimits.zw); float parallaxThicknessWorldUnits = uvstart.z / (76); //approximate this because I can't transmit any more details worldZ = worldPos.z + parallaxThicknessWorldUnits * lerp((1 - HeightEnteringPixel), (1 - HeightLeavingPixel), tHit); return float4(uvHit, testCount, 1); } } #else { if (HeightEnteringPixel >= sampledPixelBottom && HeightLeavingPixel <= sampledPixelTop) { clip(sampledPixelTop - sampledPixelBottom - 0.01); float parallaxThicknessWorldUnits = uvstart.z / (76); //approximate this because I can't transmit any more details if (HeightEnteringPixel >= sampledPixelTop) { //we've hit the top of the pixel worldZ = worldPos.z + (1 - sampledPixelTop) * parallaxThicknessWorldUnits; //height is the pixel top value return float4(uvToTest, 0.5, 1); } //we've hit a side of the high pixel worldZ = worldPos.z + (1 - HeightLeavingPixel) * parallaxThicknessWorldUnits; //height is the value we enter the pixel at return float4(uvToTest, 0.0, 1); } } #endif if (next_t > 1) //we're past the end of the test line, but didn't find any pixel to draw return float4(0, 0, testCount, 0); float2 cmp = step(t_max.xy, t_max.yx); horizontalOrVertical = cmp.y; t_max += delta * cmp; p += stp * cmp; HeightEnteringPixel = HeightLeavingPixel; //Normalized depth along the ray, expressed in “height space” } return float4(0, 0, 0, 0); } return float4(0, 0, 0, 0); }