A 3D rendering of the Zuul logo.
The GLSL shader:
/* Zuul-ci pyramid logo in 3D
Copyright © 2019 Tristan De Cacqueray
SPDX short identifier: MIT & CC-BY-NC-SA-4.0
Code is mainly based on
https://www.shadertoy.com/view/Xds3zN
Uploaded by iq in 2013-03-25
and
https://www.shadertoy.com/view/ldKGWW
Uploaded by wjbgrafx on 2016-02-04
More resources:
http://www.iquilezles.org/www/material/nvscene2008/rwwtt.pdf
http://www.iquilezles.org/www/articles/distfunctions/distfunctions.htm
*/
// Set to 1 if too slow
#define AA 4
// Uncomment to add scene elements
// #define SKY
// #define MONSTER
// #define FLOOR
// Params
uniform vec3 iResolution;
uniform float iTime;
uniform vec4 iMouse;
const vec3 cam = vec3(0., -.2, -2.);
#define PI 3.141592653589793
#define PITCH 0.428
#define YAW -0.8
#define DEG60 0.866025
// Primitives
const mat3 rotX180 = mat3(1.0, 0.0, 0.0,
0.0, cos(PI), sin(PI),
0.0, -sin(PI), cos(PI));
float smin(float a, float b, float k) {
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
return mix(b, a, h) - k * h * (1.0 - h);
}
vec2 opU(vec2 d1, vec2 d2) {
return (d1.x < d2.x) ? d1 : d2;
}
float pMirror(inout float p, float dist) {
float s = sign(p);
p = abs(p) - dist;
return s;
}
float sdPlane(vec3 p) {
return p.y;
}
float sdTriPrism(vec3 p, vec2 h) {
vec3 q = abs(p);
return max(q.z - h.y,
max(q.x * 0.4 + p.y * 0.5, -p.y) - h.x * 0.5);
}
float sdPrismZ(vec3 p, float angleRads, float height, float depth) {
vec3 q = abs(p);
return max(q.z - depth,
max(q.x * angleRads + p.y * 0.5, -p.y) - height * 0.5);
}
float sdPrismX(vec3 p, float angleRads, float height, float depth) {
vec3 q = abs(p);
return max(q.x - depth,
max(q.z * angleRads + p.y * 0.5, -p.y) - height * 0.5);
}
float sdPyramid(vec3 p, float angleRads, float height, float depth) {
vec3 q = abs(p);
return max(sdPrismX(p, angleRads, height, depth),
sdPrismZ(p, angleRads, height, depth));
}
float sdBox(vec3 p, vec3 b) {
vec3 d = abs(p) - b;
return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, 0.0));
}
float dot2(vec3 v) {
return dot(v,v);
}
float udTriangle(vec3 p, vec3 a, vec3 b, vec3 c) {
vec3 ba = b - a; vec3 pa = p - a;
vec3 cb = c - b; vec3 pb = p - b;
vec3 ac = a - c; vec3 pc = p - c;
vec3 nor = cross(ba, ac);
return sqrt(
(sign(dot(cross(ba,nor),pa)) +
sign(dot(cross(cb,nor),pb)) +
sign(dot(cross(ac,nor),pc)) < 2.0)
?
min(min(
dot2(ba*clamp(dot(ba,pa)/dot2(ba),0.0,1.0)-pa),
dot2(cb*clamp(dot(cb,pb)/dot2(cb),0.0,1.0)-pb)),
dot2(ac*clamp(dot(ac,pc)/dot2(ac),0.0,1.0)-pc))
:
dot(nor,pa)*dot(nor,pa)/dot2(nor));
}
// Zuul logo primitives
float sdHollowPyramid(vec3 p) {
float pyramid = sdPyramid(p, DEG60, 1.0, 1.0);
pyramid = max(pyramid, -sdPyramid(p - vec3(0.0, 0., 0.5), DEG60, .9, .9));
pyramid = max(pyramid, -sdBox(p, vec3(.45, 0.45, 0.45)));
return pyramid;
}
float sdHollowBox(vec3 pos, vec3 size, float hole) {
float box = sdBox(pos, size);
box = max(box, -sdBox(pos, size * vec3(2., hole, hole)));
box = max(box, -sdBox(pos, size * vec3(hole, hole, 2.)));
return box;
}
float sdInnerFrame(vec3 pos) {
float left = sdHollowBox(pos + vec3(0.06, .1, .0), vec3(.39, .48, .45), 0.87);
float right = sdHollowBox(pos + vec3(.0, .1, -.06), vec3(.45, .48, .39), 0.87);
return left < right ? left : right;
}
float sdPilar(vec3 pos) {
return sdBox(pos + vec3(.000, .15, -.424), vec3(.026, .35, .026));
}
float sdRoof(vec3 pos) {
return sdTriPrism((pos - vec3(.44, .492, .0)) * rotX180, vec2(.05, .45));
}
float sdZuul(vec3 pos) {
vec3 p = pos;
pMirror(p.x, 0.0);
pMirror(p.z, 0.0);
float pyramid = sdHollowPyramid(p);
float mainBox = sdHollowBox(p, vec3(.45, .517, .45), 0.87);
float innerBox = sdInnerFrame(p);
float lowerPlateau = sdBox(p - vec3(.0, .21, .0), vec3(.45, .026, .45));
float pilar = sdPilar(p);
float entranceWall = udTriangle(
p, vec3(.78, -.45, .45), vec3(.39, -.45, .451), vec3(.39, .34, .451));
float roof = sdRoof(p);
float closer = pyramid;
closer = smin(closer, lowerPlateau, .01);
closer = smin(closer, entranceWall, .01);
closer = closer < mainBox ? closer : mainBox;
closer = closer < roof ? closer : roof;
closer = closer < innerBox ? closer : innerBox;
closer = closer < pilar ? closer : pilar;
float socle = sdBox(pos + vec3(.0, .6, .0), vec3(2., .1, 2.));
return max(closer, -socle);
}
#ifdef MONSTER
// Julia - Quaternion
// https://www.shadertoy.com/view/MsfGRr
const int numIterations = 11;
vec4 qsqr(vec4 a) {
return vec4(a.x*a.x - a.y*a.y - a.z*a.z - a.w*a.w,
2.0*a.x*a.y,
2.0*a.x*a.z,
2.0*a.x*a.w);
}
float juliaQ(vec3 p, vec4 c) {
vec4 z = vec4(p, 0.0) * 3.6;
float md2 = 20.0;
float mz2 = dot(z, z);
vec4 trap = vec4(abs(z.xyz), dot(z, z));
for (int i=0; i < numIterations; i++) {
md2 *= 4.0 * mz2;
z = qsqr(z) + c;
trap = min(trap, vec4(abs(z.xyz), dot(z, z)));
mz2 = dot(z, z);
if (mz2 > 4.0)
break;
}
return 0.25 * sqrt(mz2 / md2) * log(mz2);
}
#endif
// The scene
vec2 scene(vec3 pos) {
vec2 res = vec2(sdZuul(pos), 29.);
#ifdef FLOOR
float plane = sdPlane(pos + vec3(.0, 1., .0));
// Does not work well, but tries to remove the floor when under it...
if (plane > .0)
res = opU(res, vec2(plane, 1.));
#endif
#ifdef MONSTER
vec4 c = 0.45*cos(vec4(0.5,3.9,1.4,1.1) + (iTime*1.5)*vec4(1.2,1.7,1.3,2.5)) - vec4(0.3,0.0,0.0,0.0);
res = opU(res, vec2(juliaQ(pos + vec3(.0, .13, .0), c), 49.));
#endif
return res;
}
vec3 calcNormal(vec3 pos) {
vec2 e = vec2(1.0, -1.0) * 0.5773 * 0.0005;
return normalize(e.xyy * scene(pos + e.xyy).x +
e.yyx * scene(pos + e.yyx).x +
e.yxy * scene(pos + e.yxy).x +
e.xxx * scene(pos + e.xxx).x);
}
float calcAO(vec3 pos, vec3 nor) {
float occ = 0.0;
float sca = 1.0;
for (int i=0; i < 5; i++) {
float hr = 0.01 + 0.12 * float(i) / 4.0;
vec3 aopos = nor * hr + pos;
float dd = scene(aopos).x;
occ += -(dd - hr) * sca;
sca *= 0.95;
}
return clamp(1.0 - 3.0 * occ, 0.0, 1.0);
}
vec2 castRay(vec3 ro, vec3 rd) {
const float tmin = 0.2;
const float tmax = 20.0;
float t = tmin;
float m;
for (int i=0; i < 300; i++) {
vec2 res = scene(ro + rd * t);
if (res.x < 0.0001 ||
t > tmax) break;
t += res.x;
m = res.y;
}
if (t > tmax) m = -1.0;
return vec2(t, m);
}
// Improved soft shadow by Sebastian Aaltonen
// From: http://www.iquilezles.org/www/articles/rmshadows/rmshadows.htm
float calcSoftshadow(vec3 ro, vec3 rd, float mint, float maxt) {
float k = 32.;
float res = 1.0;
float ph = 1e20;
for (float t=mint; t < maxt;) {
float h = scene(ro + rd * t).x;
if (h < 0.001)
return 0.0;
float y = h * h / (2.0 * ph);
float d = sqrt(h * h - y * y);
res = min(res, k * d / max(0.0, t-y));
ph = h;
t += h;
}
return res;
}
#ifdef FLOOR
// http://iquilezles.org/www/articles/checkerfiltering/checkerfiltering.htm
float checkersGradBox(vec2 p) {
// filter kernel
vec2 w = fwidth(p) + 0.001;
// analytical integral (box filter)
vec2 i = 2.0*(abs(fract((p-0.5*w)*0.5)-0.5)-abs(fract((p+0.5*w)*0.5)-0.5))/w;
// xor pattern
return 0.5 - 0.5*i.x*i.y;
}
#endif
#ifdef SKY
vec3 skyColor(vec3 rd) {
float offset = (.95 - clamp(rd.y, 0.0, 0.2))*.7;
return vec3(.5)+vec3(.5)*cos(6.28*(vec3(1.)*offset+vec3(0.0,0.10,0.20)));
}
#endif
vec3 render(vec3 ro, vec3 rd) {
vec3 col = vec3(.0);
#ifdef SKY
col = skyColor(rd);
#endif
vec2 res = castRay(ro, rd);
float t = res.x;
float m = res.y;
if (m > 0.) {
vec3 pos = ro + t * rd;
vec3 nor = calcNormal(pos);
vec3 ref = reflect(rd, nor);
col = 0.45 + 0.35*sin(vec3(0.0001,0.004,0.04)*(m-1.0));
#ifdef FLOOR
if (m < 1.5) {
float f = checkersGradBox(5.0*pos.xz);
col = 0.01 + f*vec3(0.1);
}
#endif
// lightning
float occ = calcAO(pos, nor);
vec3 lig = normalize(vec3(0.25, 0.7, -1.));
vec3 hal = normalize(lig - rd);
float amb = clamp(0.5 + 0.5 * nor.y, 0.0, 1.0);
float dif = clamp(dot(nor, lig), 0.0, 1.0);
float bac = clamp(dot(nor, normalize(vec3(-lig.x, 0.0, -lig.z))),
0.0, 1.0)*clamp(1.0 - pos.y, 0.0, 1.0);
float dom = smoothstep(-0.2, 0.2, ref.y);
float fre = pow(clamp(1.0+dot(nor,rd),0.0,1.0), 2.0);
dif *= calcSoftshadow(pos, lig, 0.02, 2.5) * 0.2;
dom *= calcSoftshadow(pos, ref, 0.02, 2.5);
float spe = pow(clamp(dot(nor, hal), 0.0, 1.0), 16.0);
spe *= dif * (0.04 + 0.96 * pow(clamp(1.0 + dot(hal, rd), 0.0, 1.0), 5.0));
vec3 lin = vec3(0.0);
lin += 3.30 * dif * vec3(1.00, 0.80, 0.55);
lin += 0.40 * amb * vec3(0.40, 0.60, 1.00) * occ;
lin += 0.40 * dom * vec3(0.40, 0.60, 1.00) * occ;
lin += 0.50 * bac * vec3(0.25, 0.25, 0.25) * occ;
lin += 0.25 * fre * vec3(1.00, 1.00, 1.00) * occ;
col *= lin;
col += 50.0 * spe * vec3(1.00, 0.90, 0.70);
col = mix(col, vec3(.0), 1.0 - exp(-0.0002 * t * t * t));
}
#ifdef SKY
// fog
float rayDist = length(ro + rd * t);
col = mix(col, skyColor(rd), 1.0 - 1.0 / exp(rayDist * 0.05));
#endif
return vec3(clamp(col, 0.0, 1.0));
}
float smoothStair(float frame) {
return frame - sin(frame) / 1.9;
}
mat3 camRotation() {
float yaw, pitch;
if (iMouse.z > 0.0) {
yaw = (iMouse.x / iResolution.x - 0.5) * 4.;
pitch = (iMouse.y / iResolution.y - 0.5) * 4.;
} else {
yaw = -.35 + smoothStair(iTime * 2.) / 2.;
pitch = PITCH;
}
return mat3(1.0, 0.0, 0.0,
0.0, cos(pitch), -sin(pitch),
0.0, sin(pitch), cos(pitch)) *
mat3(cos(yaw), 0.0, sin(yaw),
0.0, 1.0, 0.0,
-sin(yaw), 0.0, cos(yaw));
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
mat3 rot = camRotation();
vec3 tot = vec3(0.0);
#if AA > 1
for(int m=0; m < AA; m++)
for(int n=0; n < AA; n++) {
vec2 o = vec2(float(m), float(n)) / float(AA) - 0.5;
vec2 uv = (gl_FragCoord.xy+o) / iResolution.xy*2.-1.;
#else
vec2 uv = gl_FragCoord.xy / iResolution.xy*2.-1.;
#endif
uv.y *= iResolution.y / iResolution.x;
vec3 dir = normalize(vec3(uv, 1.)) * rot;
vec3 pos = cam * rot;
vec3 col = render(pos, dir);
// gamma
col = pow(col, vec3(0.4));
tot += col;
#if AA > 1
}
tot /= float(AA * AA);
#endif
fragColor = vec4(tot, 1.0);
}
void main(void) {
mainImage(gl_FragColor, gl_FragCoord.xy);
}
The renderer:
# Zuul-ci pyramid logo in 3D
# SPDX short identifier: MIT
"""Render the zuul.glsl shader to a zuul.mp4 file at 25 fps"""
from subprocess import Popen
import numpy as np
from glumpy import app, gl, gloo
from PIL import Image
RES = [500, 500]
ROTATION = 2
FPS = 25
vertex = "attribute vec2 p; void main(void) {gl_Position = vec4(p, 0.0, 1.0);}"
fragment = open("zuul.glsl").read()
window = app.Window(width=RES[0], height=RES[1])
program = gloo.Program(vertex, fragment, count=4)
program['p'] = [(-1, -1), (-1, +1), (+1, -1), (+1, +1)]
program['iResolution'] = RES + [0]
gl.glEnable(gl.GL_BLEND)
gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
backend = app.__backend__
clock = app.__init__(backend=backend, framerate=FPS)
pixels = np.zeros((RES[0], RES[1] * 3), dtype=np.uint8)
for i in range(int(np.pi * ROTATION * FPS)):
window.activate()
window.clear()
program["iTime"] = i / FPS
program.draw(gl.GL_TRIANGLE_STRIP)
gl.glReadPixels(
0, 0, RES[0], RES[1], gl.GL_RGB, gl.GL_UNSIGNED_BYTE, pixels)
image = Image.frombytes(
"RGB", RES, np.ascontiguousarray(np.flip(pixels, 0)))
image.save("%03d.png" % (i + 1), 'png')
backend.process(clock.tick())
Popen(["ffmpeg", "-y", "-r", str(FPS), "-i", "%03d.png", "-pix_fmt", "yuv420p",
"zuul.mp4"]).wait()