Procedural Texturing #
Problem statement #
Exercise
Adapt other patterns from the book of shaders and map them as textures onto other 3D shapes.
Background #
A procedural texture is a texture created using a mathematical description like an algorithm rather than directly stored data. The advantage of this approach is low storage cost, unlimited texture resolution and easy texture mapping. These kinds of textures are often used to model surface or volumetric representations of natural elements such as wood, marble, granite, metal, stone, and others.
Usually, the natural look of the rendered result is achieved by the usage of fractal noise and turbulence functions. These functions are used as a numerical representation of the “randomness” found in nature.
Truchet #
Truchet tiles are square tiles decorated with patterns that are not rotationally symmetric. When placed in a square tiling of the plane, they can form varied patterns, and the orientation of each tile can be used to visualize information associated with the tile’s position within the tiling. Truchet tiles were first described in a 1704 memoir by Sébastien Truchet entitled “Mémoire sur les combinaisons”, and were popularized in 1987 by Cyril Stanley Smith.
Code #
Code Procedural Texturing .js
let pg;
let truchetShader, brickShader,colorShader;
let theShader;
let selectShader;
let selectShape;
let shapeValue = 0;
function preload() {
// shader adapted from here: https://thebookofshaders.com/09/
truchetShader = readShader('/vc_page/sketches/shaders/procedural/truchet.frag',
{ matrices: Tree.NONE, varyings: Tree.NONE });
brickShader = readShader('/vc_page/sketches/shaders/procedural/brick.frag',
{ matrices: Tree.NONE, varyings: Tree.NONE });
colorShader = readShader('/vc_page/sketches/shaders/procedural/color.frag',
{ matrices: Tree.NONE, varyings: Tree.NONE });
}
function setup() {
createCanvas(400, 400, WEBGL);
// create frame buffer object to render the procedural texture
pg = createGraphics(400, 400, WEBGL);
textureMode(NORMAL);
noStroke();
pg.noStroke();
pg.textureMode(NORMAL);
selectShader = createSelect();
selectShader.position(10, 10);
selectShader.style('color', 'black');
selectShader.option('Truchet', 0);
selectShader.option('Brick', 1);
selectShader.option('Color', 2);
selectShader.changed(selectShaderEvent);
selectShape = createSelect();
selectShape.position(300, 10);
selectShape.style('color', 'black');
selectShape.option('Cylinder', 0);
selectShape.option('Sphere', 1);
selectShape.option('Box', 2);
selectShape.changed(selectShapeEvent);
theShader = truchetShader;
// use truchetShader to render onto pg
pg.shader(theShader);
// emitResolution, see:
// https://github.com/VisualComputing/p5.treegl#macros
pg.emitResolution(theShader);
// https://p5js.org/reference/#/p5.Shader/setUniform
theShader.setUniform('u_zoom', 3);
// pg clip-space quad (i.e., both x and y vertex coordinates ∈ [-1..1])
pg.quad(-1, -1, 1, -1, 1, 1, -1, 1);
// set pg as texture
texture(pg);
}
function selectShaderEvent(){
shaderValue = selectShader.value();
if(shaderValue == 0){
theShader = truchetShader;
}else if(shaderValue == 1){
theShader = brickShader;
console.log('brick');
}else if(shaderValue == 2){
theShader = colorShader;
}
pg.shader(theShader);
// emitResolution, see:
// https://github.com/VisualComputing/p5.treegl#macros
pg.emitResolution(theShader);
// https://p5js.org/reference/#/p5.Shader/setUniform
theShader.setUniform('u_zoom', 3);
// pg clip-space quad (i.e., both x and y vertex coordinates ∈ [-1..1])
pg.quad(-1, -1, 1, -1, 1, 1, -1, 1);
// set pg as texture
texture(pg);
}
function selectShapeEvent(){
shapeValue = selectShape.value();
}
function draw() {
background(33);
orbitControl();
if (shapeValue == 0){
cylinder(100, 200);
} else if (shapeValue == 1){
sphere(120);
} else {
box(150,150,150);
}
}
function mouseMoved() {
// https://p5js.org/reference/#/p5.Shader/setUniform
theShader.setUniform('u_zoom', int(map(mouseX, 0, width, 1, 30)));
// pg clip-space quad (i.e., both x and y vertex coordinates ∈ [-1..1])
pg.quad(-1, -1, 1, -1, 1, 1, -1, 1);
}
Code truchet.frag
// Author @patriciogv ( patriciogonzalezvivo.com ) - 2015
#ifdef GL_ES
precision mediump float;
#endif
#define PI 3.14159265358979323846
uniform vec2 u_resolution;
uniform float u_time;
uniform float u_zoom;
vec2 rotate2D (vec2 _st, float _angle) {
_st -= 0.5;
_st = mat2(cos(_angle),-sin(_angle),
sin(_angle),cos(_angle)) * _st;
_st += 0.5;
return _st;
}
vec2 tile (vec2 _st, float _zoom) {
_st *= _zoom;
return fract(_st);
}
vec2 rotateTilePattern(vec2 _st){
// Scale the coordinate system by 2x2
_st *= 2.0;
// Give each cell an index number
// according to its position
float index = 0.0;
index += step(1., mod(_st.x,2.0));
index += step(1., mod(_st.y,2.0))*2.0;
// |
// 2 | 3
// |
//--------------
// |
// 0 | 1
// |
// Make each cell between 0.0 - 1.0
_st = fract(_st);
// Rotate each cell according to the index
if(index == 1.0){
// Rotate cell 1 by 90 degrees
_st = rotate2D(_st,PI*0.5);
} else if(index == 2.0){
// Rotate cell 2 by -90 degrees
_st = rotate2D(_st,PI*-0.5);
} else if(index == 3.0){
// Rotate cell 3 by 180 degrees
_st = rotate2D(_st,PI);
}
return _st;
}
void main (void) {
vec2 st = gl_FragCoord.xy/u_resolution.xy;
st = tile(st,u_zoom);
st = rotateTilePattern(st);
// Make more interesting combinations
// st = tile(st,2.0);
// st = rotate2D(st,-PI*u_time*0.25);
// st = rotateTilePattern(st*2.);
// st = rotate2D(st,PI*u_time*0.25);
// step(st.x,st.y) just makes a b&w triangles
// but you can use whatever design you want.
gl_FragColor = vec4(vec3(step(st.x,st.y)),1.0);
}
Code brick.frag
// Author @patriciogv ( patriciogonzalezvivo.com ) - 2015
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
uniform float u_time;
uniform float u_zoom;
vec2 brickTile(vec2 _st, float _zoom){
_st *= _zoom;
// Here is where the offset is happening
_st.x += step(1., mod(_st.y,2.0)) * 0.5;
return fract(_st);
}
float box(vec2 _st, vec2 _size){
_size = vec2(0.5)-_size*0.5;
vec2 uv = smoothstep(_size,_size+vec2(1e-4),_st);
uv *= smoothstep(_size,_size+vec2(1e-4),vec2(1.0)-_st);
return uv.x*uv.y;
}
void main(void){
vec2 st = gl_FragCoord.xy/u_resolution.xy;
vec3 color = vec3(0.0);
// Modern metric brick of 215mm x 102.5mm x 65mm
// http://www.jaharrison.me.uk/Brickwork/Sizes.html
// st /= vec2(2.15,0.65)/1.5;
// Apply the brick tiling
st = brickTile(st,u_zoom);
color = vec3(box(st,vec2(0.9)));
// Uncomment to see the space coordinates
//color = vec3(st,0.0);
gl_FragColor = vec4(color,1.0);
}
Code color.frag
// Author @patriciogv - 2015
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
uniform float u_time;
uniform float u_zoom;
float circle(in vec2 _st, in float _radius){
vec2 l = _st-vec2(0.5);
return 1.-smoothstep(_radius-(_radius*0.01),
_radius+(_radius*0.01),
dot(l,l)*4.0);
}
void main() {
vec2 st = gl_FragCoord.xy/u_resolution;
vec3 color = vec3(0.0);
st *= u_zoom; // Scale up the space by 3
st = fract(st); // Wrap around 1.0
// Now we have 9 spaces that go from 0-1
color = vec3(st,0.0);
//color = vec3(circle(st,0.5));
gl_FragColor = vec4(color,1.0);
}
Conclusions #
The advantage of using procedural texturing is low storage cost, unlimited texture resolution and easy texture mapping.
Manually drawing the geometries required to achieve textures can be challenging, and it can be even more challenging to scale and conform them to complex parts. Scalability is where procedural texturing can offer considerable advantages over manually modeling surface features.