# Py5 loves 3D (P3D, hints, shaders)

**URL:** <https://discourse.processing.org/t/py5-loves-3d-p3d-hints-shaders/43821>\
**Category:** py5\
**Created:** [February 3, 2024, 2:05am UTC](https://discourse.processing.org/t/py5-loves-3d-p3d-hints-shaders/43821 "2024-02-03T02:05:26Z")\
**Posts on this page:** 10\
**Page:** 1

<div class="post-metadata">

**Author:** ![villares](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.processing.org/villares/32/3166_2.png) [@villares](https://discourse.processing.org/u/villares)\
**Post date:** [February 3, 2024, 2:05am UTC](https://discourse.processing.org/t/py5-loves-3d-p3d-hints-shaders/43821/1 "2024-02-03T02:05:26Z")

</div>

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](https://discourse.processing.org/t/program-to-test-hint-with-transparency/4361)

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](https://github.com/villares/sketch-a-day/blob/main/2024/sketch_2024_02_01/sketch_2024_02_01.py)

 ![image](https://canada1.discourse-cdn.com/flex036/uploads/processingfoundation1/original/3X/9/4/94abda908f7b6ceaef8a6213af0fc6f9c132de60.png)

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

> <https://github.com/villares/sketch-a-day/blob/main/2024/sketch_2024_01_30/sketch_2024_01_30.py>

---

<div class="post-metadata">

**Author:** ![GoToLoop](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.processing.org/gotoloop/32/86_2.png) [@GoToLoop](https://discourse.processing.org/u/GoToLoop)\
**Post date:** [February 3, 2024, 4:31am UTC](https://discourse.processing.org/t/py5-loves-3d-p3d-hints-shaders/43821/2 "2024-02-03T04:31:40Z")

</div>

> [@villares](#):
>
> `from sdf import sphere, box, cylinder, X, Y, Z`

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

> [@villares](#):
>
> `import trimesh`

pip install trimesh

> [@villares](#):
>
> ```auto
> #f.save('out.stl')
> m = trimesh.load_mesh('out.stl')
> 
> ```

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”:

```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”:

```auto
#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”:

```auto
// 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];
}

```

* * *

---

<div class="post-metadata">

**Author:** ![villares](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.processing.org/villares/32/3166_2.png) [@villares](https://discourse.processing.org/u/villares)\
**Post date:** [February 3, 2024, 1:33pm UTC](https://discourse.processing.org/t/py5-loves-3d-p3d-hints-shaders/43821/3 "2024-02-03T13:33:02Z")

</div>

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](https://gitlab.com/nobodyinperson/sdfcad)

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` 😉

---

<div class="post-metadata">

**Author:** ![GoToLoop](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.processing.org/gotoloop/32/86_2.png) [@GoToLoop](https://discourse.processing.org/u/GoToLoop)\
**Post date:** [February 3, 2024, 4:28pm UTC](https://discourse.processing.org/t/py5-loves-3d-p3d-hints-shaders/43821/4 "2024-02-03T16:28:08Z")

</div>

> [@villares](#):
>
> Personally I’m moving all my stuff from `os.path` to `pathlib.Path` 😉

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?

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

```

Compare that to **isfile()**:

```py
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.

---

<div class="post-metadata">

**Author:** ![villares](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.processing.org/villares/32/3166_2.png) [@villares](https://discourse.processing.org/u/villares)\
**Post date:** [February 3, 2024, 4:38pm UTC](https://discourse.processing.org/t/py5-loves-3d-p3d-hints-shaders/43821/5 "2024-02-03T16:38:33Z")

</div>

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 ☹

---

<div class="post-metadata">

**Author:** ![GoToLoop](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.processing.org/gotoloop/32/86_2.png) [@GoToLoop](https://discourse.processing.org/u/GoToLoop)\
**Post date:** [February 8, 2024, 1:21am UTC](https://discourse.processing.org/t/py5-loves-3d-p3d-hints-shaders/43821/6 "2024-02-08T01:21:19Z")

</div>

I’ve converted the “Toon Shading” py5 sketch to PyScript + p5\*js + Proceso:

> **[PyScript](https://pyscript.com/@gotoloop/toon-shading)**

* * *

## “sketch.py”:

```auto
"""
  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”:

```auto
<!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”:

```auto
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.

---

<div class="post-metadata">

**Author:** ![hx2A](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.processing.org/hx2a/32/14322_2.png) [@hx2A](https://discourse.processing.org/u/hx2A)\
**Post date:** [February 8, 2024, 2:21am UTC](https://discourse.processing.org/t/py5-loves-3d-p3d-hints-shaders/43821/7 "2024-02-08T02:21:03Z")

</div>

> [@villares](#):
>
> 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, `glob()`! I use that one all the time.

> [@villares](#):
>
> Also, sadly, I think a `path object` breaks the trimesh/sdf load/save functions ☹

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

---

<div class="post-metadata">

**Author:** ![villares](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.processing.org/villares/32/3166_2.png) [@villares](https://discourse.processing.org/u/villares)\
**Post date:** [February 8, 2024, 7:42pm UTC](https://discourse.processing.org/t/py5-loves-3d-p3d-hints-shaders/43821/8 "2024-02-08T19:42:51Z")

</div>

> [@hx2A](#):
>
> 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 🙂

---

<div class="post-metadata">

**Author:** ![villares](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.processing.org/villares/32/3166_2.png) [@villares](https://discourse.processing.org/u/villares)\
**Post date:** [February 8, 2024, 7:43pm UTC](https://discourse.processing.org/t/py5-loves-3d-p3d-hints-shaders/43821/9 "2024-02-08T19:43:56Z")

</div>

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

---

<div class="post-metadata">

**Author:** ![hx2A](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.processing.org/hx2a/32/14322_2.png) [@hx2A](https://discourse.processing.org/u/hx2A)\
**Post date:** [February 8, 2024, 11:09pm UTC](https://discourse.processing.org/t/py5-loves-3d-p3d-hints-shaders/43821/10 "2024-02-08T23:09:45Z")

</div>

> [@villares](#):
>
> I made a “Merge Request” to enable Path objects on the GitLab repo for `sdf-fork` and they accepted it 🙂

Excellent! Spreading the word about `pathlib.Path`!
