This article will go through the steps of constructing an infinite square tunnel with a minecraft-like feel (pixelated colors). We'll go through multiple simplified code samples to show what each part does - with screenshots.
fn gradient_color(value: f32) -> vec3<f32> { // Clamp the input value to the range [0.0, 1.0] let t = clamp(value, 0.0, 1.0);
// Define gradient colors let colors = array<vec3<f32>, 5>( vec3<f32>(1.0, 1.0, 1.0), // White vec3<f32>(0.75, 0.75, 0.75), // Gray vec3<f32>(0.87, 0.72, 0.53), // Light Brown vec3<f32>(0.65, 0.45, 0.25), // Brown vec3<f32>(0.45, 0.27, 0.07) // Dark Brown );
// Determine segment and local interpolation factor let segment = t * 4.0; // Map t to range [0, 4] let index = floor(segment); // Segment index let local_t = fract(segment); // Local interpolation factor
// Interpolate between two consecutive colors return mix(colors[i32(index)], colors[i32(index + 1)], local_t); }
var normalized_coords = (2.0 * (input_coords) - 1.0); // -1.0 to 1.0
var abs_coords = abs( normalized_coords ); // 1->0->1
var quantized_coords = floor( 10.0 * abs_coords );
var rv = random2( quantized_coords );
var rgb = gradient_color( rv );
return vec4<f32>( rgb, 1.0 ); }
Other gradient colors - repeating values to emphasis particular colors.
const numGradients:i32 = 8; fn gradient_color(value: f32) -> vec3<f32> { // Clamp the input value to the range [0.0, 1.0] let t = clamp(value, 0.0, 1.0);
// Define gradient colors let colors = array<vec3<f32>, numGradients>( vec3<f32>(1.0, 1.0, 1.0), // White vec3<f32>(1.0), vec3<f32>(0.75, 0.75, 0.75), vec3<f32>(0.75, 0.75, 0.75), // Gray vec3<f32>(0.75, 0.75, 0.75), // Gray vec3<f32>(0.37, 0.52, 0.33), vec3<f32>(0.65, 0.45, 0.25), // Brown vec3<f32>(0.45, 0.27, 0.07) // Dark Brown );
// Determine segment and local interpolation factor let segment = t * ( f32(numGradients) - 1.0 ); // Map t to range [0, numGradients] let index = floor(segment); // Segment index let local_t = fract(segment); // Local interpolation factor
// Interpolate between two consecutive colors return mix(colors[i32(index)], colors[i32(index + 1)], local_t); }
Towards Infinity
Simple example of taking a screen full of squares
floor(..)
function and the uv coordinates - then the vertical texture coordinate that goes from 0 to 1 - which is used for the
depth
(divide it by the distance so as it goes further away it gets smaller).
We need to make sur ew use the normalized coordinates with the origin in the middle of the screen so that is the far distance point.
@fragment fn main(@location(0) input_coords: vec2<f32>) -> @location(0) vec4<f32> { // Scale the input coordinates var pixel_coords = input_coords * 512.0;
var vertical = input_coords.y; // 0 to 1
// map the 0->1 to lo->hi value let lo = 1.0; let hi = 0.1; var block_size = lo + (hi - lo) * (vertical);
// Calculate normalized coordinates -1 to 1 - we want to divide by the // normalize (0 in the middle) - so when we divide the value it goes towards // the middle and not the left (if 0,0 is on the left side). // Useful to know if you want to move the target point! var normalized_coords = (2.0 * pixel_coords - resolution.xy) / resolution.y;
var quantized_coords = floor( 10.0 * normalized_coords / block_size );
// Generate a random color based on the quantized coordinates var rr = random2(quantized_coords);
return vec4<f32>( vec3(rr), 1.0); }
If we modify the code so instead of just going from 0 to 1 for the vertical - we use the normalized value that goes from -1 to 1 - we pass it through an
abs(..)
function - so it remains positive - which gives us 1.0->0.0->1.0 for the texture coordinate.
var vertical = abs(normalized_coords.y); // 1->0->1
Gives us the following output:
This is the full code - same as before - except the modified line:
@fragment fn main(@location(0) input_coords: vec2<f32>) -> @location(0) vec4<f32> { // Scale the input coordinates var pixel_coords = input_coords * 512.0;
// Calculate normalized coordinates -1 to 1 - we want to divide by the // normalize (0 in the middle) - so when we divide the value it goes towards // the middle and not the left (if 0,0 is on the left side). // Useful to know if you want to move the target point! var normalized_coords = (2.0 * pixel_coords - resolution.xy) / resolution.y;
var vertical = 1.0 - abs(normalized_coords.x); // 1->0->1
// map the 0->1 to lo->hi value let lo = 1.0; let hi = 0.3; var block_size = lo + (hi - lo) * (vertical);
var quantized_coords = floor( 10.0 * normalized_coords / block_size );
// Generate a random color based on the quantized coordinates var rr = random2(quantized_coords);
return vec4<f32>( vec3(rr), 1.0); }
It is working - but we need to fix the values; so instead of 1-0-1 we use 0-1-0 to get the correct output.
The following will invert the coordinates and give us a better result:
var vertical = 1.0 - abs(normalized_coords.y); // 0->1->0
Simply a matter of changing the
.y
to
.x
to see the same thing for the horizontal.
var vertical = 1.0 - abs(normalized_coords.x); // 1->0->1
The big step after this - is to have both the horizontal and vertical! To create a tunnel effect.
We'll check which is the biggest value - the horizontal or vertical and use this to divide as the distance.
@fragment fn main(@location(0) input_coords: vec2<f32>) -> @location(0) vec4<f32> { // Scale the input coordinates var pixel_coords = input_coords * 512.0;
// Calculate normalized coordinates -1 to 1 - we want to divide by the // normalize (0 in the middle) - so when we divide the value it goes towards // the middle and not the left (if 0,0 is on the left side). // Useful to know if you want to move the target point! var normalized_coords = (2.0 * pixel_coords - resolution.xy) / resolution.y;
var delta = 1.0 - abs(normalized_coords); // 1->0->1 var distance = delta.y; if ( delta.y > delta.x ) { distance = delta.x; }
// map the 0->1 to lo->hi value let lo = 1.0; let hi = 0.3; var block_size = lo + (hi - lo) * (distance);
var quantized_coords = floor( 10.0 * normalized_coords / block_size );
// Generate a random color based on the quantized coordinates var rr = random2(quantized_coords);
return vec4<f32>( vec3(rr), 1.0); }
If you'll notice the squares seems stretched - and not correct, so we fix this by modifying the calculation.
@fragment fn main(@location(0) input_coords: vec2<f32>) -> @location(0) vec4<f32> { // Scale the input coordinates var pixel_coords = input_coords;
// Calculate normalized coordinates -1 to 1 - we want to divide by the // normalize (0 in the middle) - so when we divide the value it goes towards // the middle and not the left (if 0,0 is on the left side). // Useful to know if you want to move the target point! var normalized_coords = (2.0 * input_coords - 1.0);// -1.0 to 1.0
// Generate a random color based on the quantized coordinates var rr = random2( quantized_coords );
rr = mix( 0.0, rr, pow(length(abs_coords),2) );
return vec4<f32>( vec3(rr), 1.0); }
Tinker with Colors
While grayscale does look good - adding a bit of color can make things more interesting - and we'll set the ground to brown - so make some of the squares brown.
@fragment fn main(@location(0) input_coords: vec2<f32>) -> @location(0) vec4<f32> { // Scale the input coordinates var pixel_coords = input_coords;
let tt = mytimer;
var normalized_coords = (2.0 * (input_coords) - 1.0); // -1.0 to 1.0
var abs_coords = abs( normalized_coords ); // 1->0->1
// Generate a random color based on the quantized coordinates var rr = random2( quantized_coords );
let background_color = vec3<f32>(0.78, 0.57, 0.4);
// mix some squares with brown var color = mix( background_color, vec3(rr), clamp(pow(rr+0.7, 5),0.0, 1.0) );
// fade to black in the distance color = mix( vec3(0.0), color, pow(length(abs_coords),2) );
return vec4<f32>( color, 1.0); }
We can use a color gradient from the random value instead of just blending to give greater control over each squares color.
const numGradients:i32 = 8; fn gradient_color(value: f32) -> vec3<f32> { // Clamp the input value to the range [0.0, 1.0] let t = clamp(value, 0.0, 1.0);
// Determine segment and local interpolation factor let segment = t * ( f32(numGradients) - 1.0 ); // Map t to range [0, numGradients] let index = floor(segment); // Segment index let local_t = fract(segment); // Local interpolation factor
// Interpolate between two consecutive colors return mix(colors[i32(index)], colors[i32(index + 1)], local_t); }
Some other color values
const numGradients:i32 = 8; fn gradient_color(value: f32) -> vec3<f32> { // Clamp the input value to the range [0.0, 1.0] let t = clamp(value, 0.0, 1.0);
let col0 = vec3<f32>(111/255.0, 85/255.0, 60.0/255.0); let col1 = vec3<f32>(139/255.0, 118/255.0, 99.0/255.0); let col2 = vec3<f32>(45/255.0, 31/255.0, 21.0/255.0);
// Determine segment and local interpolation factor let segment = t * ( f32(numGradients) - 1.0 ); // Map t to range [0, numGradients] let index = floor(segment); // Segment index let local_t = fract(segment); // Local interpolation factor
// Interpolate between two consecutive colors return mix(colors[i32(index)], colors[i32(index + 1)], local_t); }
Minecraft Tunnel
Let's go a bit further - the tunnel is fun and interesting - but it can do with a few more tweaks! Add some multisampling to smooth out some of those jaggedy edges, also a few more colors and a train rail at the bottom.
// Generates a pseudo-random value based on a 2D input vector fn random2(point: vec2<f32>) -> f32 { let seed = dot(point, vec2<f32>(127.1, 311.7)); // Generate a hash seed using dot product return fract(sin(seed) * 43758.5453123); // Return a pseudo-random fractional value }
// Custom smoothstep function for smooth interpolation fn mysmoothstep(edge_start: f32, edge_end: f32, value: f32) -> f32 { let t = clamp((value - edge_start) / (edge_end - edge_start), 0.0, 1.0); // Clamp and normalize value return t * t * (3.0 - 2.0 * t); // Cubic Hermite interpolation for smooth transition }
// Fragment shader main function @fragment fn main(@location(0) input_coords: vec2<f32>) -> @location(0) vec4<f32> { // Scale the input coordinates var pixel_coords = input_coords;
// Initialize colors var black = vec3<f32>(0); // Base color (black) var white = vec3<f32>(1); // Secondary color (white) var total_color = black; // Accumulator for final color
// Perform anti-aliasing by sampling the pixel multiple times for (var sample_x: i32 = 0; sample_x < NUM_SAMPLES; sample_x = sample_x + 1) { for (var sample_y: i32 = 0; sample_y < NUM_SAMPLES; sample_y = sample_y + 1) { // Offset for sub-pixel sampling var sample_offset = (vec2<f32>(f32(sample_x), f32(sample_y)) / f32(NUM_SAMPLES)) - 0.5; sample_offset *= 1.0/resolution;
// Calculate normalized coordinates -1.0 to 1.0 var normalized_coords = (2.0 * pixel_coords + sample_offset - 1.0);
// Compute additional parameters for distortion and patterns let normalized_x = normalize(normalized_coords).x;
Visitor:
Copyright (c) 2002-2024 xbdev.net - All rights reserved.
Designated articles, tutorials and software are the property of their respective owners.