Py5 loves 3D (P3D, hints, shaders)

I think one can do almost anything that can done in Processing P3D with py5 (because it also uses P3D).

Yesterday I was having a look at an example inspired by a conversation started by @hamoid, with additions from @GoToLoop: Program to test hint() with transparency

So I converted my Processing Python mode sketch to py5 here: sketch-a-day/2024/sketch_2024_02_01/sketch_2024_02_01.py at main · villares/sketch-a-day · GitHub

Another recent exploration was using shaders, ported from a Processing example:

5 Likes

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

Thanks, @GoToLoop, for the thoughtful additions!

The PyPI sdf-fork situation is specially awkward…
UPDATE: I’m trying sdfcad now instead of sdf-fork, it looks like it is actively maintained at Yann Büchau / sdfCAD · GitLab

I should mention that pip install trimesh[easy] will install other optional but useful stuff, specially mapox-earcut without which you get some nasty exceptions when triangulating polys.

Personally I’m moving all my stuff from os.path to pathlib.Path :wink:

1 Like

I didn’t know about class pathlib.Path. Still very oblivious even to Python’s standard library!

So, we’d need to instantiate class pathlib.Path 1st before reaching its method is_file(), right?

from pathlib import Path
Path(filename).is_file() or create_stl_file(filename)

Compare that to isfile():

from os.path import isfile
isfile(filename) or create_stl_file(filename)

If we need to execute different kinds of operations over the same path, the Path class version is better.

Otherwise, functions from module os.path are much simpler IMHO.

So both modules have cases which they’re more appropriate. The newer doesn’t deprecate the older.

1 Like

Sure, os.path works fine in this case and in many cases, very straight forward!

I just wanted to tell you how much I’m enjoying pathlib.Path for some other more complex stuff. I really like stuff like .parent, .name, the / operator for concatenating paths, lot’s of stuff. iterdir() can return complete paths…

Also, sadly, I think a path object breaks the trimesh/sdf load/save functions :frowning:

1 Like

I’ve converted the “Toon Shading” py5 sketch to PyScript + p5*js + Proceso:
https://PyScript.com/@gotoloop/toon-shading


“sketch.py”:

"""
  Toon Shading by @villares (2024/Jan/30)
  PyScript + p5*js + Proceso version by @GoToLoop (2024/Feb/07)

  https://Discourse.Processing.org/t/py5-loves-3d-pd3-hints-shaders/43821/6
  https://PyScript.com/@gotoloop/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
"""

from proceso import Sketch # https://Proceso.cc

py5 = Sketch()
py5.describe('toon effects')

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.WEBGL)

def preload():
    global shape, toon

    shape = py5.load_model(STL_FILE)
    toon = py5._p5js.loadShader(VERT_FILE, FRAG_FILE)


def setup():
    settings()

    py5.fill(0o320)
    py5.no_stroke()

    shape.clearColors()

    global bg, shape_scale

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


def draw():
    py5.background(bg)

    lights_enabled and py5.lights()

    py5.directional_light(204, 204, 204, 0, 0, -1)
    py5.rotate_x(py5.mouse_y * MOUSE_STEP)
    py5.rotate_y(py5.mouse_x * MOUSE_STEP)

    py5.scale(shape_scale)
    py5.model(shape)


def mouse_pressed():
    global shader_enabled, lights_enabled

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

    lights_enabled ^= py5.mouse_button == py5.RIGHT


__name__ == '__main__' and py5.run_sketch(
    preload = preload, setup = setup, draw = draw, mouse_pressed = mouse_pressed
)

“index.html”:

<!DOCTYPE html>

<meta charset=utf-8>
<title>ASCII Visualization</title>
<link rel=icon href=favicon.ico>

<script async src=//cdn.JsDelivr.net/npm/p5></script>

<link rel=stylesheet href=//Unpkg.com/@pyscript/core/dist/core.css>
<script type=module src=//Unpkg.com/@pyscript/core></script>

<script type=py src=sketch.py config=pyscript.toml></script>

“pyscript.toml”:

name = 'Toon Shading'
version = '1.0.1'
description = 'https://Discourse.Processing.org/t/py5-loves-3d-pd3-hints-shaders/43821/6'
url = 'https://PyScript.com/@gotoloop/toon-shading'
packages = [ 'proceso' ]

However, I couldn’t make the shaders to work. Perhaps Processing’s GLSL is incompatible w/ p5*js.

2 Likes

Also, glob()! I use that one all the time.

Unfortunately not all libraries support Python’s pathlib.Path objects. py5 does though!

I made a “Merge Request” to enable Path objects on the GitLab repo for sdf-fork and they accepted it :slight_smile:

This is seriously impressive! @mcintyre will like seeing this!

2 Likes

Excellent! Spreading the word about pathlib.Path!