Py5 loves 3D (P3D, hints, shaders)

Package’s name is “sdf-fork”: pip install sdf-fork

pip install trimesh

No mention we need to uncomment f.save('out.stl') in order to have the STL file.

So I’ve added some code to execute f.save('out.stl') if it doesn’t exist already:


“sketch_2024_01_30.py”:

"""
 * Toon Shading
 *
 * Example showing the use of a custom lighting shader in order to apply
 * a "toon" effect on the scene. Based on the glsl tutorial from lighthouse 3D:
 * https://Lighthouse3d.com/tutorials/glsl-tutorial/toon-shader-version-ii/
 *
 * https://Discourse.Processing.org/t/py5-loves-3d-pd3-hints-shaders/43821/2
"""

import py5 # https://py5Coding.org/content/install.html

from trimesh import load_mesh # pip install -U trimesh[easy]
from sdf import sphere, box, cylinder, X, Y, Z # pip install sdfcad or sdf-fork

from os.path import isfile

STL_FILE, FRAG_FILE, VERT_FILE = 'out.stl', 'ToonFrag.glsl', 'ToonVert.glsl'
MOUSE_STEP, SHAPE_SCALE = .01, .4

shader_enabled = lights_enabled = False

def settings(): py5.size(700, 550, py5.P3D)

def setup():
    py5.fill(0o320)
    py5.no_stroke()

    global shape, toon, shape_scale, cx, cy

    shape = py5.convert_shape(mesh)
    shape.disable_style()

    toon = py5.load_shader(FRAG_FILE, VERT_FILE)

    shape_scale = min(py5.width, py5.height) * SHAPE_SCALE

    cx, cy = py5.width >> 1, py5.height >> 1


def draw():
    py5.background(0)

    lights_enabled and py5.lights()

    py5.directional_light(204, 204, 204, 0, 0, -1)
    py5.translate(cx, cy)

    py5.rotate_x(py5.mouse_y * MOUSE_STEP)
    py5.rotate_y(py5.mouse_x * MOUSE_STEP)

    py5.scale(shape_scale)
    py5.shape(shape, 0, 0)


def mouse_pressed():
    global shader_enabled, lights_enabled

    shader_enabled ^= py5.mouse_button == py5.LEFT
    py5.shader(toon) if shader_enabled else py5.reset_shader()

    lights_enabled ^= py5.mouse_button == py5.RIGHT


def create_stl_file(filename=STL_FILE):
    c = cylinder(.5)
    f = sphere(1) & box(1.5)
    f -= c.orient(X) | c.orient(Y) | c.orient(Z)
    f.save(filename)


def main(filename=STL_FILE):
    isfile(filename) or create_stl_file(filename)

    global mesh
    mesh = load_mesh(filename)

    py5.run_sketch(sketch_functions = SKETCH_CALLBACKS_DICT)


SKETCH_CALLBACKS = settings, setup, draw, mouse_pressed
SKETCH_CALLBACKS_DICT = { funct.__name__: funct for funct in SKETCH_CALLBACKS }

__name__ == '__main__' and main()

“ToonFrag.glsl”:

#version 330 core

precision mediump float;
precision mediump int;

in vec3 vertNormal, vertLightDir;
out vec4 fragColor;

const vec4 colors[] = {
  vec4(.2, .15, .15, 1),
  vec4(.4, .25, .25, 1),
  vec4(.6, .35, .35, 1),
  vec4(1, .5, .5, 1)
};

void main() {
  fragColor = colors[ int(4 * clamp(dot(vertLightDir, vertNormal), 0, 1)) ];
}

“ToonVert.glsl”:

// Toon shader using per-pixel lighting. Based on the glsl
// tutorial from lighthouse 3D:
// https://Lighthouse3d.com/tutorials/glsl-tutorial/toon-shader-version-ii/

#version 330 core
#define PROCESSING_LIGHT_SHADER

precision mediump float;

uniform vec3 lightNormal[8];

uniform mat4 transform;
uniform mat3 normalMatrix;

in vec4 vertex;
in vec3 normal;

out vec3 vertNormal, vertLightDir;

void main() {
  // Vertex in clip coordinates.
  gl_Position = transform * vertex;

  // Normal the vector if eye coordinates are passed
  // to the fragment shader.
  vertNormal = normalize(normalMatrix * normal);

  // Assuming that there is only one directional light,
  // its normal vector is passed to the fragment shader
  // in order to perform per-pixel lighting calculation.
  vertLightDir = -lightNormal[0];
}

2 Likes