<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Shading on blog</title>
    <link>https://jpcharalambosh.co/tags/shading/</link>
    <description>Recent content in Shading on blog</description>
    <image>
      <title>blog</title>
      <url>https://jpcharalambosh.co/papermod-cover.png</url>
      <link>https://jpcharalambosh.co/papermod-cover.png</link>
    </image>
    <generator>Hugo</generator>
    <language>en</language>
    <lastBuildDate>Thu, 09 Apr 2026 15:52:34 -0500</lastBuildDate>
    <atom:link href="https://jpcharalambosh.co/tags/shading/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Toon shading</title>
      <link>https://jpcharalambosh.co/posts/toon/</link>
      <pubDate>Thu, 09 Apr 2026 15:52:34 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/toon/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Toon shading, or &lt;a href=&#34;https://en.wikipedia.org/wiki/Cel_shading&#34;&gt;cel shading&lt;/a&gt;, gives 3D geometry a flat, cartoon-like look by quantizing diffuse reflection into a finite number of discrete &lt;code&gt;shades&lt;/code&gt;. In this p5.js v2 version the shader is written as a &lt;a href=&#34;https://beta.p5js.org/reference/p5/basematerialshader/&#34;&gt;&lt;code&gt;baseMaterialShader().modify()&lt;/code&gt;&lt;/a&gt; hook using the &lt;a href=&#34;https://beta.p5js.org/tutorials/intro-to-p5-strands/&#34;&gt;p5.strands&lt;/a&gt; DSL — no raw GLSL strings, no hand-written vertex shader. &lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;&lt;code&gt;createPanel&lt;/code&gt;&lt;/a&gt; wires the color and shades controls to the shader automatically each frame.&lt;/p&gt;
&lt;/blockquote&gt;
















&lt;iframe
  id=&#39;p5-0&#39;
  style=&#39;border:none; display:block; width:0; height:0;&#39;
  srcdoc=&#34;
    &lt;!DOCTYPE html&gt;
    &lt;html&gt;
      &lt;head&gt;
        &lt;style&gt;body{margin:0;padding:0;overflow:hidden}&lt;/style&gt;
        &lt;script src=&#39;https://cdn.jsdelivr.net/npm/p5@2.2.3/lib/p5.min.js&#39;&gt;&lt;/script&gt;
        
        &lt;script src=&#39;https://cdn.jsdelivr.net/npm/p5.tree/dist/p5.tree.min.js&#39;&gt;&lt;/script&gt;
        
        
        
        
        
        
        
        
        &lt;script&gt;
          
&amp;#39;use strict&amp;#39;

let models, modelsDisplayed
let toon, panel
const depth = 0.4

function setup() {
  createCanvas(600, 400, WEBGL)

  panel = createPanel({
    u_shades: { min: 1, max: 10, value: 5, step: 1,   label: &amp;#39;shades&amp;#39; },
    u_tint:   { value: &amp;#39;#ffd700&amp;#39;,                      label: &amp;#39;tint&amp;#39;   }
  }, { x: 10, y: 10, labels: true, color: &amp;#39;white&amp;#39; })

  toon = baseMaterialShader().modify({
    uniforms: {
      &amp;#39;float u_shades&amp;#39;:   5,
      &amp;#39;vec3  u_tint&amp;#39;:     [1, 0.84, 0],
      &amp;#39;vec3  u_lightDir&amp;#39;: [0, 0, 1],
      &amp;#39;vec3  u_color&amp;#39;:    [1, 1, 1]
    },
    &amp;#39;vec4 combineColors&amp;#39;: `(ColorComponents components) {
      float intensity = max(0.0, dot(normalize(vNormal), normalize(u_lightDir)));
      float shadeSize = 1.0 / clamp(u_shades, 1.0, 10.0);
      float k         = floor(intensity / shadeSize) * shadeSize;
      k = max(k, shadeSize * 0.5);
      return vec4(k * u_tint * u_color, 1.0);
    }`
  })

  colorMode(RGB, 1)
  noStroke()
  setAttributes(&amp;#39;antialias&amp;#39;, true)
  document.oncontextmenu = () =&amp;gt; false

  const trange = 200
  models = []
  for (let i = 0; i &amp;lt; 100; i&amp;#43;&amp;#43;) {
    models.push({
      position: createVector(
        (random() * 2 - 1) * trange,
        (random() * 2 - 1) * trange,
        (random() * 2 - 1) * trange
      ),
      angle: random(0, TWO_PI),
      axis:  p5.Vector.random3D(),
      size:  random() * 50 &amp;#43; 16,
      color: [random(), random(), random()]
    })
  }

  modelsDisplayed = createSlider(1, models.length, int(models.length / 2), 1)
  modelsDisplayed.position(width - 125, 15)
  modelsDisplayed.style(&amp;#39;width&amp;#39;, &amp;#39;120px&amp;#39;)
}

function draw() {
  orbitControl()
  background(&amp;#39;#1C1D1F&amp;#39;)

  push()
  stroke(&amp;#39;green&amp;#39;)
  axes({ size: 175 })
  grid({ size: 175 })
  pop()

  const lx  = (mouseX / width  - 0.5) * 2
  const ly  = (mouseY / height - 0.5) * 2
  const len = Math.sqrt(lx*lx &amp;#43; ly*ly &amp;#43; depth*depth)
  const tv  = panel.u_tint.value()

  shader(toon)
  toon.setUniform(&amp;#39;u_lightDir&amp;#39;, [lx/len, ly/len, depth/len])
  toon.setUniform(&amp;#39;u_shades&amp;#39;,   panel.u_shades.value())
  toon.setUniform(&amp;#39;u_tint&amp;#39;,     [tv[0], tv[1], tv[2]])

  noStroke()

  for (let i = 0; i &amp;lt; modelsDisplayed.value(); i&amp;#43;&amp;#43;) {
    toon.setUniform(&amp;#39;u_color&amp;#39;, models[i].color)
    push()
    translate(models[i].position)
    rotate(models[i].angle, models[i].axis)
    const r = models[i].size / 2
    if      (i % 3 === 0) cone(r)
    else if (i % 3 === 1) sphere(r)
    else                  torus(r, r / 4)
    pop()
  }

  resetShader()
}

function mouseWheel() { return false }

        &lt;/script&gt;
        
        &lt;script&gt;
          (function() {
            var t = setInterval(function() {
              var c = document.querySelector(&#39;canvas&#39;)
              if (c &amp;&amp; c.offsetWidth &gt; 0) {
                clearInterval(t)
                window.parent.postMessage({ type: &#39;p5resize&#39;, id: &#39;p5-0&#39;, w: c.offsetWidth, h: c.offsetHeight }, &#39;*&#39;)
              }
            }, 100)
          })()
        &lt;/script&gt;
      &lt;/head&gt;
      &lt;body&gt;&lt;/body&gt;
    &lt;/html&gt;
  &#34;
&gt;&lt;/iframe&gt;
&lt;script&gt;
window.addEventListener(&#39;message&#39;, function(e) {
  if (e.data &amp;&amp; e.data.type === &#39;p5resize&#39; &amp;&amp; e.data.id === &#39;p5-0&#39;) {
    var f = document.getElementById(&#39;p5-0&#39;)
    if (f) { f.style.width = e.data.w + &#39;px&#39;; f.style.height = e.data.h + &#39;px&#39; }
  }
})
&lt;/script&gt;



&lt;hr&gt;
&lt;h1 id=&#34;toon-shader&#34;&gt;Toon shader&lt;/h1&gt;
&lt;p&gt;The &lt;code&gt;combineColors&lt;/code&gt; hook in &lt;code&gt;baseMaterialShader().modify()&lt;/code&gt; receives the eye-space &lt;code&gt;vNormal&lt;/code&gt; varying directly — no vertex shader boilerplate required. Diffuse intensity is the dot product of the surface normal and the (normalized) light direction; it is then snapped into discrete bands to produce the cel-shading look.&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
