<?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>Graphics on blog</title>
    <link>https://jpcharalambosh.co/tags/graphics/</link>
    <description>Recent content in Graphics 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>Sat, 25 Apr 2026 15:00:00 -0500</lastBuildDate>
    <atom:link href="https://jpcharalambosh.co/tags/graphics/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Visibility testing</title>
      <link>https://jpcharalambosh.co/posts/visibility_testing/</link>
      <pubDate>Sat, 25 Apr 2026 15:00:00 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/visibility_testing/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Per-object frustum culling with &lt;a href=&#34;https://github.com/VisualComputing/p5.tree#visibility-testing&#34;&gt;&lt;code&gt;visibility&lt;/code&gt;&lt;/a&gt;. Sixty mixed primitives drift around in a wide volume; each frame their bounds — AABBs for boxes, spheres for spheres — are tested against a fixed test camera&amp;rsquo;s frustum, with the six frustum planes pre-computed once per frame via &lt;a href=&#34;https://github.com/VisualComputing/p5.tree#visibility-testing&#34;&gt;&lt;code&gt;bounds({ mat4Eye })&lt;/code&gt;&lt;/a&gt;. Three return states drive the render: solid lit material for &lt;code&gt;VISIBLE&lt;/code&gt;, amber wireframe for &lt;code&gt;SEMIVISIBLE&lt;/code&gt; (the bounds straddle a plane), faint grey for &lt;code&gt;INVISIBLE&lt;/code&gt;. Toggle the test camera&amp;rsquo;s projection between perspective and orthographic with the gold checkbox; drag to orbit the observer and watch the cyan wireframe stay put while objects drift through it.&lt;/p&gt;</description>
    </item>
    <item>
      <title>CPU proximity picking</title>
      <link>https://jpcharalambosh.co/posts/cpu_proximity_picking/</link>
      <pubDate>Sat, 25 Apr 2026 14:30:00 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/cpu_proximity_picking/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Zero-GPU hit testing with &lt;a href=&#34;https://github.com/VisualComputing/p5.tree#cpu-proximity-picking&#34;&gt;&lt;code&gt;mouseHit&lt;/code&gt;&lt;/a&gt;. Ten waypoint nodes around a soft loop — each one tested every frame against the cursor by projecting its origin to screen space and comparing against a configurable radius. The hit zone is drawn explicitly with &lt;a href=&#34;https://github.com/VisualComputing/p5.tree#cpu-proximity-picking&#34;&gt;&lt;code&gt;bullsEye&lt;/code&gt;&lt;/a&gt;, which takes the same &lt;code&gt;size&lt;/code&gt; and &lt;code&gt;shape&lt;/code&gt; parameters as &lt;code&gt;mouseHit&lt;/code&gt; — the gizmo &lt;em&gt;is&lt;/em&gt; the hit zone, no guessing where the click registers. Use the panel to tune the radius and switch between circular and square hit shapes.&lt;/p&gt;</description>
    </item>
    <item>
      <title>GPU color-ID picking</title>
      <link>https://jpcharalambosh.co/posts/gpu_color_id_picking/</link>
      <pubDate>Sat, 25 Apr 2026 14:00:00 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/gpu_color_id_picking/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Pixel-perfect mouse picking with &lt;a href=&#34;https://github.com/VisualComputing/p5.tree#gpu-color-id-picking&#34;&gt;&lt;code&gt;mousePick&lt;/code&gt;&lt;/a&gt;. Fifty mixed primitives — boxes, spheres, torii, cones, cylinders — drawn at random positions and tagged with a unique integer id encoded as a CSS hex colour by &lt;a href=&#34;https://github.com/VisualComputing/p5.tree#gpu-color-id-picking&#34;&gt;&lt;code&gt;tag&lt;/code&gt;&lt;/a&gt;. Each frame the scene renders twice: once into a 1×1 framebuffer with each shape filled by its id, then &lt;code&gt;gl.readPixels&lt;/code&gt; returns whichever id sits under the cursor; the visible pass renders normally and lights up the hit. Cost is one extra geometry submission per frame regardless of object count, and the answer is exactly the rendered pixel — through the hole of a torus, behind a partially-occluding box, anywhere a real fragment landed.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Camera interpolation</title>
      <link>https://jpcharalambosh.co/posts/camera_interpolation/</link>
      <pubDate>Sun, 19 Apr 2026 12:00:00 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/camera_interpolation/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Lookat-camera keyframe animation with &lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;&lt;code&gt;createCameraTrack&lt;/code&gt;&lt;/a&gt;. A bound &lt;code&gt;animCam&lt;/code&gt; plays a four-keyframe track; a separate &lt;code&gt;viewCam&lt;/code&gt; orbits around the abstraction — eye polyline, gaze rays, per-keyframe mini-camera markers. A live frustum follows playback, and an FBO inset shows what &lt;code&gt;animCam&lt;/code&gt; actually sees. Toggle &lt;code&gt;TEX&lt;/code&gt; and that inset slides onto the live frustum&amp;rsquo;s near plane — the frustum becomes a window. Two &lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;&lt;code&gt;createPanel&lt;/code&gt;&lt;/a&gt; instances drive the whole thing: one binds checkboxes to UI state, one binds transport controls to the track.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Pose interpolation</title>
      <link>https://jpcharalambosh.co/posts/pose_interpolation/</link>
      <pubDate>Sun, 19 Apr 2026 10:00:00 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/pose_interpolation/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TRS keyframe animation with &lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;&lt;code&gt;createPoseTrack&lt;/code&gt;&lt;/a&gt;. Four &lt;code&gt;{ pos, rot }&lt;/code&gt; keyframes, cubic Hermite interpolation of position with auto-Catmull-Rom tangents, and slerp on rotations. A transport panel scrubs the track; a second panel toggles the &lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;&lt;code&gt;trackPath&lt;/code&gt;&lt;/a&gt; overlay bits — PATH, CONTROLS, TANGENTS_IN, TANGENTS_OUT — and switches interpolation modes live. Unlike &lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;&lt;code&gt;createCameraTrack&lt;/code&gt;&lt;/a&gt;, which applies its result to a camera automatically, &lt;code&gt;PoseTrack&lt;/code&gt; produces an interpolated pose you fold into the transform stack yourself with &lt;code&gt;applyPose(track.eval(out))&lt;/code&gt;.&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 track
let uiTrack, uiViz
const out = { pos: [0, 0, 0], rot: [0, 0, 0, 1], scl: [1, 1, 1] }

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

  // ── PoseTrack ────────────────────────────────────────────────────────────
  track = createPoseTrack()
  track.add({ pos: [-220, -40,   0], rot: { axis: [0, 1, 0], angle:  0         } })
  track.add({ pos: [ -70, -40,  80], rot: { axis: [0, 1, 0], angle:  Math.PI/3 } })
  track.add({ pos: [  70, -40, -80], rot: { axis: [0, 1, 0], angle: -Math.PI/4 } })
  track.add({ pos: [ 220, -40,   0], rot: { axis: [0, 1, 0], angle:  Math.PI/2 } })

  track.play({ loop: true, bounce: true, duration: 90 })

  // ── Transport panel ──────────────────────────────────────────────────────
  uiTrack = createPanel(track, {
    x: 10, y: 10, width: 130,
    title: &amp;#39;PoseTrack&amp;#39;, info: true,
    reset: false, camera: null,
    color: &amp;#39;white&amp;#39;,
  })

  // ── Path viz &amp;#43; interp modes ─────────────────────────────────────────────
  uiViz = createPanel({
    path:        { value: true },
    controls:    { value: true },
    tangentsIn:  { value: true },
    tangentsOut: { value: true },
    posInterp:   { type: &amp;#39;select&amp;#39;, value: &amp;#39;hermite&amp;#39;,
                   options: [{ label: &amp;#39;hermite&amp;#39;, value: &amp;#39;hermite&amp;#39; },
                             { label: &amp;#39;linear&amp;#39;,  value: &amp;#39;linear&amp;#39;  },
                             { label: &amp;#39;step&amp;#39;,    value: &amp;#39;step&amp;#39;    }] },
    rotInterp:   { type: &amp;#39;select&amp;#39;, value: &amp;#39;slerp&amp;#39;,
                   options: [{ label: &amp;#39;slerp&amp;#39;, value: &amp;#39;slerp&amp;#39; },
                             { label: &amp;#39;nlerp&amp;#39;, value: &amp;#39;nlerp&amp;#39; },
                             { label: &amp;#39;step&amp;#39;,  value: &amp;#39;step&amp;#39;  }] },
  }, {
    x: 460, y: 10, width: 130, labels: true,
    title: &amp;#39;Path viz&amp;#39;, color: &amp;#39;white&amp;#39;,
  })

  noFill()
  strokeWeight(1.2)
}

function draw() {
  background(&amp;#39;#0a0a0e&amp;#39;)
  orbitControl()

  stroke(70, 90, 110)
  grid({ size: 400, subdivisions: 8 })

  // live interp modes — read once per frame
  track.posInterp = uiViz.posInterp.value()
  track.rotInterp = uiViz.rotInterp.value()

  // path overlays — each bit in its own ambient stroke colour
  const { PATH, CONTROLS, TANGENTS_IN, TANGENTS_OUT } = p5.Tree
  if (uiViz.path.value())        { stroke(&amp;#39;#e8ecf1&amp;#39;); trackPath(track, { bits: PATH,         marker: null }) }
  if (uiViz.controls.value())    { stroke(&amp;#39;#4a5566&amp;#39;); trackPath(track, { bits: CONTROLS,     marker: null }) }
  if (uiViz.tangentsIn.value())  { stroke(&amp;#39;#5cd0ff&amp;#39;); trackPath(track, { bits: TANGENTS_IN,  marker: null }) }
  if (uiViz.tangentsOut.value()) { stroke(&amp;#39;#ff6ec7&amp;#39;); trackPath(track, { bits: TANGENTS_OUT, marker: null }) }

  // per-keyframe markers — default six-axis cross oriented by the keyframe&amp;#39;s pose
  stroke(180)
  trackPath(track, { bits: 0 })

  // interpolated object at the track cursor
  track.eval(out)
  push()
  applyPose(out)
  stroke(&amp;#39;#ffd166&amp;#39;)
  noFill()
  box(42)
  pop()
}

const mouseWheel = () =&amp;gt; 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;four-keyframes-one-object&#34;&gt;Four keyframes, one object&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;createPoseTrack&lt;/code&gt; returns a &lt;code&gt;PoseTrack&lt;/code&gt; — a renderer-agnostic state machine for &lt;code&gt;{ pos, rot, scl }&lt;/code&gt; keyframes. &lt;code&gt;track.add(spec)&lt;/code&gt; appends one; adjacent duplicates are skipped by default:&lt;/p&gt;</description>
    </item>
    <item>
      <title>Depth-of-field blur with focal target</title>
      <link>https://jpcharalambosh.co/posts/blur/</link>
      <pubDate>Thu, 09 Apr 2026 15:52:34 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/blur/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A depth-of-field blur effect where the focal plane is driven by a live world-space position. The scene is rendered into a &lt;a href=&#34;https://beta.p5js.org/reference/p5/createframebuffer/&#34;&gt;&lt;code&gt;p5.Framebuffer&lt;/code&gt;&lt;/a&gt;, then a &lt;a href=&#34;https://beta.p5js.org/tutorials/intro-to-p5-strands/&#34;&gt;p5.strands&lt;/a&gt; DOF pass is applied via &lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;&lt;code&gt;pipe()&lt;/code&gt;&lt;/a&gt;. The magenta sphere is the focal target — its screen-space z is recomputed every frame with &lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;&lt;code&gt;mapLocation()&lt;/code&gt;&lt;/a&gt; and fed directly into the shader, so the blur follows the sphere continuously. A first-person directional light tracks the viewer using &lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;&lt;code&gt;mapDirection()&lt;/code&gt;&lt;/a&gt;.&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;

// zero-alloc buffers — allocated once, reused every frame
const _dir = new Float32Array(3)
const _loc = new Float32Array(3)

let layer, dofFilter, models, panel
let focusVal = 0

function dofCallback() {
  const depthTex      = uniformTexture(() =&amp;gt; layer.depth)
  const focus         = uniformFloat(() =&amp;gt; focusVal)
  const blurIntensity = uniformFloat(&amp;#39;blurIntensity&amp;#39;)

  const getBlurriness = (d) =&amp;gt; abs(d - focus) * 40 * blurIntensity
  const maxBlurDist   = (b) =&amp;gt; b * 0.01

  getColor((inputs, canvasContent) =&amp;gt; {
    let colour = getTexture(canvasContent, inputs.texCoord)
    let samples = 1
    const centerDepth = getTexture(depthTex, inputs.texCoord).r
    const blur = getBlurriness(centerDepth)
    for (let i = 0; i &amp;lt; 20; i&amp;#43;&amp;#43;) {
      const angle  = float(i) * TWO_PI / 20
      const dist   = float(i) / 20 * maxBlurDist(blur)
      const offset = [cos(angle), sin(angle)] * dist
      const sampleDepth = getTexture(depthTex, inputs.texCoord &amp;#43; offset).r
      if (sampleDepth &amp;gt;= centerDepth ||
          maxBlurDist(getBlurriness(sampleDepth)) &amp;gt;= dist) {
        colour &amp;#43;= getTexture(canvasContent, inputs.texCoord &amp;#43; offset)
        samples&amp;#43;&amp;#43;
      }
    }
    colour /= float(samples)
    return [colour.rgb, 1]
  })
}

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

  layer     = createFramebuffer()
  dofFilter = baseFilterShader().modify(dofCallback)

  panel = createPanel({
    blurIntensity: { min: 0, max: 4, value: 2, step: 0.1, label: &amp;#39;blur&amp;#39; }
  }, { target: dofFilter, x: 10, y: 10, width: 140, labels: true, color: &amp;#39;white&amp;#39; })

  const trange = 200
  models = []
  for (let i = 0; i &amp;lt; 50; i&amp;#43;&amp;#43;) {
    models.push({
      position: createVector(
        (random() * 2 - 1) * trange,
        (random() * 2 - 1) * trange,
        (random() * 2 - 1) * trange
      ),
      size:  random() * 25 &amp;#43; 8,
      color: i === 0 ? color(255, 0, 220)
                     : color(int(random(256)), int(random(256)), int(random(256))),
      type:  i === 0 ? &amp;#39;ball&amp;#39; : i &amp;lt; 25 ? &amp;#39;torus&amp;#39; : &amp;#39;box&amp;#39;
    })
  }
}

function draw() {
  layer.begin()
  background(0)
  axes()
  orbitControl()
  noStroke()
  ambientLight(100)

  // first-person directional light — camera forward in world space
  mapDirection(p5.Tree._k, { out: _dir, from: p5.Tree.EYE, to: p5.Tree.WORLD })
  directionalLight(255, 255, 255, _dir[0], _dir[1], _dir[2])

  specularMaterial(255)
  shininess(150)

  models.forEach(m =&amp;gt; {
    push()
    fill(m.color)
    translate(m.position)
    m.type === &amp;#39;box&amp;#39;   ? box(m.size)   :
    m.type === &amp;#39;torus&amp;#39; ? torus(m.size) : sphere(m.size)
    pop()
  })

  // focal depth from the magenta sphere&amp;#39;s world position → screen z
  mapLocation(models[0].position, { out: _loc, from: p5.Tree.WORLD, to: p5.Tree.SCREEN })
  focusVal = _loc[2]
  layer.end()

  pipe(layer, [dofFilter])
}

const mouseWheel = () =&amp;gt; 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;the-shader-as-a-strands-callback&#34;&gt;The shader as a strands callback&lt;/h1&gt;
&lt;p&gt;The DOF pass is authored as a &lt;a href=&#34;https://beta.p5js.org/tutorials/intro-to-p5-strands/&#34;&gt;&lt;code&gt;p5.strands&lt;/code&gt;&lt;/a&gt; callback on &lt;code&gt;baseFilterShader().modify()&lt;/code&gt;. There is no raw GLSL string — the algorithm is expressed in JavaScript using the strands DSL, which compiles it to WebGL2 under the hood.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Post-effects pipeline</title>
      <link>https://jpcharalambosh.co/posts/post_effects/</link>
      <pubDate>Thu, 09 Apr 2026 15:52:34 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/post_effects/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Multi-pass post-processing with &lt;a href=&#34;https://beta.p5js.org/tutorials/intro-to-p5-strands/&#34;&gt;p5.strands&lt;/a&gt; and &lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;&lt;code&gt;pipe()&lt;/code&gt;&lt;/a&gt;. Three filter passes — depth-of-field blur, value-noise warp, and pixelation — are chained over a scene framebuffer. Each pass is a &lt;code&gt;baseFilterShader().modify()&lt;/code&gt; callback; &lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;&lt;code&gt;createPanel&lt;/code&gt;&lt;/a&gt; wires their uniforms automatically. Press &lt;code&gt;1&lt;/code&gt;, &lt;code&gt;2&lt;/code&gt;, or &lt;code&gt;3&lt;/code&gt; to rotate the pass ordering at runtime.&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;

const _dir = new Float32Array(3)
const _loc = new Float32Array(3)

let layer, models
let dofFilter, noiseFilter, pixelFilter
let uiDof, uiNoise, uiPixel
let focusVal = 0
let order = 0   // 0=dof→noise→pixel  1=noise→pixel→dof  2=pixel→dof→noise

// ── Strands callbacks ─────────────────────────────────────────────────────────

function dofCallback() {
  const depthTex      = uniformTexture(() =&amp;gt; layer.depth)
  const focus         = uniformFloat(() =&amp;gt; focusVal)
  const blurIntensity = uniformFloat(&amp;#39;blurIntensity&amp;#39;)
  const getBlurriness = (d) =&amp;gt; abs(d - focus) * 40 * blurIntensity
  const maxBlurDist   = (b) =&amp;gt; b * 0.01
  getColor((inputs, canvasContent) =&amp;gt; {
    let colour = getTexture(canvasContent, inputs.texCoord)
    let samples = 1
    const centerDepth = getTexture(depthTex, inputs.texCoord).r
    const blur = getBlurriness(centerDepth)
    for (let i = 0; i &amp;lt; 20; i&amp;#43;&amp;#43;) {
      const angle  = float(i) * TWO_PI / 20
      const dist   = float(i) / 20 * maxBlurDist(blur)
      const offset = [cos(angle), sin(angle)] * dist
      const sd     = getTexture(depthTex, inputs.texCoord &amp;#43; offset).r
      if (sd &amp;gt;= centerDepth || maxBlurDist(getBlurriness(sd)) &amp;gt;= dist) {
        colour &amp;#43;= getTexture(canvasContent, inputs.texCoord &amp;#43; offset)
        samples&amp;#43;&amp;#43;
      }
    }
    colour /= float(samples)
    return [colour.rgb, 1]
  })
}

function noiseCallback() {
  const frequency = uniformFloat(&amp;#39;frequency&amp;#39;)
  const amplitude = uniformFloat(&amp;#39;amplitude&amp;#39;)
  const speed     = uniformFloat(&amp;#39;speed&amp;#39;)
  const t         = uniformFloat(() =&amp;gt; millis() / 1000)
  const hash      = (p) =&amp;gt; fract(sin(dot(p, [127.1, 311.7, 74.7])) * 43758.5453123)
  const fade      = (t) =&amp;gt; t * t * (3 - 2 * t)
  const valueNoise = (p) =&amp;gt; {
    const i = floor(p), f = fract(p), u = fade(f)
    const n000=hash(i&amp;#43;[0,0,0]), n100=hash(i&amp;#43;[1,0,0])
    const n010=hash(i&amp;#43;[0,1,0]), n110=hash(i&amp;#43;[1,1,0])
    const n001=hash(i&amp;#43;[0,0,1]), n101=hash(i&amp;#43;[1,0,1])
    const n011=hash(i&amp;#43;[0,1,1]), n111=hash(i&amp;#43;[1,1,1])
    const nx00=mix(n000,n100,u.x), nx10=mix(n010,n110,u.x)
    const nx01=mix(n001,n101,u.x), nx11=mix(n011,n111,u.x)
    return mix(mix(nx00,nx10,u.y), mix(nx01,nx11,u.y), u.z) * 2 - 1
  }
  getColor((inputs, canvasContent) =&amp;gt; {
    const s  = frequency * inputs.texCoord.x
    const v  = frequency * inputs.texCoord.y
    const n1 = valueNoise([s,      v, speed * t])
    const n2 = valueNoise([s &amp;#43; 17, v, speed * t])
    return [getTexture(canvasContent,
              inputs.texCoord &amp;#43; [amplitude*n1, amplitude*n2]).rgb, 1]
  })
}

function pixelCallback() {
  const level = uniformFloat(&amp;#39;level&amp;#39;)
  getColor((inputs, canvasContent) =&amp;gt; {
    const snapped = floor(inputs.texCoord * level) / level
    return [getTexture(canvasContent, snapped).rgb, 1]
  })
}

// ── Setup ─────────────────────────────────────────────────────────────────────

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

  layer       = createFramebuffer()
  dofFilter   = baseFilterShader().modify(dofCallback)
  noiseFilter = baseFilterShader().modify(noiseCallback)
  pixelFilter = baseFilterShader().modify(pixelCallback)

  uiDof = createPanel({
    blurIntensity: { min: 0, max: 4,   value: 1.5, step: 0.1, label: &amp;#39;blur&amp;#39;      }
  }, { target: dofFilter,   x: 10,  y: 10, width: 130, labels: true, title: &amp;#39;DOF&amp;#39;,   color: &amp;#39;white&amp;#39; })

  uiNoise = createPanel({
    frequency: { min: 0, max: 10, value: 3,   step: 0.1,  label: &amp;#39;frequency&amp;#39; },
    amplitude: { min: 0, max: 1,  value: 0.3, step: 0.01, label: &amp;#39;amplitude&amp;#39; },
    speed:     { min: 0, max: 1,  value: 0.3, step: 0.01, label: &amp;#39;speed&amp;#39;     }
  }, { target: noiseFilter, x: 155, y: 10, width: 130, labels: true, title: &amp;#39;Noise&amp;#39;, color: &amp;#39;white&amp;#39; })

  uiPixel = createPanel({
    level: { min: 2, max: 600, value: 300, step: 1, label: &amp;#39;level&amp;#39; }
  }, { target: pixelFilter, x: 300, y: 10, width: 130, labels: true, title: &amp;#39;Pixel&amp;#39;, color: &amp;#39;white&amp;#39; })

  const trange = 200
  models = []
  for (let i = 0; i &amp;lt; 50; i&amp;#43;&amp;#43;) {
    models.push({
      position: createVector(
        (random() * 2 - 1) * trange,
        (random() * 2 - 1) * trange,
        (random() * 2 - 1) * trange
      ),
      size:  random() * 25 &amp;#43; 8,
      color: i === 0 ? color(255, 0, 220)
                     : color(int(random(256)), int(random(256)), int(random(256))),
      type:  i === 0 ? &amp;#39;ball&amp;#39; : i &amp;lt; 25 ? &amp;#39;torus&amp;#39; : &amp;#39;box&amp;#39;
    })
  }
}

// ── Draw ──────────────────────────────────────────────────────────────────────

function draw() {
  layer.begin()
  background(0)
  axes()
  orbitControl()
  noStroke()
  ambientLight(100)
  mapDirection(p5.Tree._k, { out: _dir, from: p5.Tree.EYE, to: p5.Tree.WORLD })
  directionalLight(255, 255, 255, _dir[0], _dir[1], _dir[2])
  specularMaterial(255)
  shininess(150)
  models.forEach(m =&amp;gt; {
    push()
    fill(m.color)
    translate(m.position)
    m.type === &amp;#39;box&amp;#39; ? box(m.size) : m.type === &amp;#39;torus&amp;#39; ? torus(m.size) : sphere(m.size)
    pop()
  })
  mapLocation(models[0].position, { out: _loc, from: p5.Tree.WORLD, to: p5.Tree.SCREEN })
  focusVal = _loc[2]
  layer.end()

  const passes = [[dofFilter, noiseFilter, pixelFilter],
                  [noiseFilter, pixelFilter, dofFilter],
                  [pixelFilter, dofFilter, noiseFilter]][order]
  pipe(layer, passes)

  beginHUD()
  fill(255)
  noStroke()
  textSize(12)
  text([&amp;#39;dof → noise → pixel&amp;#39;,
        &amp;#39;noise → pixel → dof&amp;#39;,
        &amp;#39;pixel → dof → noise&amp;#39;][order] &amp;#43; &amp;#39;  (1/2/3 to rotate)&amp;#39;,
       10, height - 10)
  endHUD()
}

function keyPressed() {
  if (key === &amp;#39;1&amp;#39; || key === &amp;#39;2&amp;#39; || key === &amp;#39;3&amp;#39;)
    order = (order &amp;#43; 1) % 3
}

const mouseWheel = () =&amp;gt; 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;shader-passes-as-strands-callbacks&#34;&gt;Shader passes as strands callbacks&lt;/h1&gt;
&lt;p&gt;Each post-processing pass is a &lt;code&gt;baseFilterShader().modify()&lt;/code&gt; callback — a plain JavaScript function using the &lt;a href=&#34;https://beta.p5js.org/tutorials/intro-to-p5-strands/&#34;&gt;p5.strands&lt;/a&gt; DSL. The framework compiles each callback to WebGL2 at startup; no raw GLSL strings are needed.&lt;/p&gt;</description>
    </item>
    <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>
    <item>
      <title>3D Brush Painting in VR</title>
      <link>https://jpcharalambosh.co/posts/brush/</link>
      <pubDate>Thu, 09 Apr 2026 12:00:00 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/brush/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A 3D brush painting sketch that uses depth control for VR-style experiences. &lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;&lt;code&gt;mapLocation()&lt;/code&gt;&lt;/a&gt; converts screen-space mouse coordinates and a depth slider value directly into world-space positions, so each brush stroke is placed precisely in 3D. Press &lt;code&gt;r&lt;/code&gt; to toggle recording, &lt;code&gt;c&lt;/code&gt; to clear, &lt;code&gt;f&lt;/code&gt; to re-focus the camera on the world origin.&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 brushColor, depthSlider, points = []
let recording = false

function setup() {
  createCanvas(600, 400, WEBGL)
  colorMode(RGB, 1)
  document.oncontextmenu = () =&amp;gt; false

  // initialise depth slider at the screen-space z of the world origin
  const o = mapLocation(p5.Tree.ORIGIN, { from: p5.Tree.WORLD, to: p5.Tree.SCREEN })
  depthSlider = createSlider(0, 1, o.z, 0.001)
  depthSlider.position(10, 10)
  depthSlider.style(&amp;#39;width&amp;#39;, &amp;#39;580px&amp;#39;)

  brushColor = createColorPicker(&amp;#39;#C7C08D&amp;#39;)
  brushColor.position(width - 70, 40)
}

function draw() {
  mouseY &amp;gt;= 30 &amp;amp;&amp;amp; orbitControl()
  recording &amp;amp;&amp;amp; addPoint()

  background(&amp;#39;#222226&amp;#39;)
  axes({ size: 50, bits: p5.Tree.X | p5.Tree.Y | p5.Tree.Z })

  for (const pt of points) {
    push()
    noStroke()
    fill(pt.color)
    translate(pt.pos)
    sphere(1)
    pop()
  }
}

function addPoint() {
  points.push({
    pos:   mapLocation([mouseX, mouseY, depthSlider.value()],
                       { from: p5.Tree.SCREEN, to: p5.Tree.WORLD }),
    color: brushColor.color()
  })
}

function focusOrigin() {
  const eye    = mapLocation()                              // camera world position
  const up     = mapDirection(p5.Tree.j)                   // camera up in world space
  const origin = [0, 0, 0]
  camera(eye.x, eye.y, eye.z, ...origin, up.x, up.y, up.z)
  const o = mapLocation(p5.Tree.ORIGIN, { from: p5.Tree.WORLD, to: p5.Tree.SCREEN })
  depthSlider.value(o.z)
}

function keyPressed() {
  key === &amp;#39;c&amp;#39; &amp;amp;&amp;amp; (points = [])
  key === &amp;#39;f&amp;#39; &amp;amp;&amp;amp; focusOrigin()
  key === &amp;#39;r&amp;#39; &amp;amp;&amp;amp; (recording = !recording)
}

        &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;h2 id=&#34;screen-to-world-mapping-with-maplocation&#34;&gt;Screen-to-world mapping with &lt;code&gt;mapLocation&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;The core of the brush is a single &lt;code&gt;mapLocation&lt;/code&gt; call that lifts the 2D mouse position — together with a depth value from the slider — into 3D world space each frame:&lt;/p&gt;</description>
    </item>
    <item>
      <title>Visualizing Perspective Transformation to NDC</title>
      <link>https://jpcharalambosh.co/posts/ndc/</link>
      <pubDate>Tue, 24 Mar 2026 10:28:01 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/ndc/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://jsantell.com/3d-projection/#perspective-projection&#34;&gt;Perspective projection&lt;/a&gt; is a fundamental concept in 3D graphics. The transformation maps a view frustum — a truncated pyramid — into a cube of Normalized Device Coordinates (NDC), producing the foreshortening effect that makes 3D scenes look convincing. The sketch below morphs a set of &lt;code&gt;cajas&lt;/code&gt; (boxes) continuously from world space into NDC space, rendered from a third-person camera that also displays the frustum being transformed.&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;

// NDC morph — p5.js v2 &amp;#43; p5.tree v0.0.25
// original: https://jpcharalambosh.co/posts/ndc/

let mainCam, frustumCam
let fillMorph, outlineMorph
let frustumProj       // p5.Matrix(4) — frustum projection, updated each frame
let vBuf, eBuf        // Float32Array(16) — frustum view / eye matrices
let cajas, panel

const N    = 81       // near plane
const NDC  = 0.5      // NDC morph offset
const FOVY = 0.75     // frustum vertical fov
const Z    = 158      // frustum camera distance

// ── setup ─────────────────────────────────────────────────────────────────────
function setup() {
  createCanvas(600, 400, WEBGL)
  strokeWeight(0.8)
  colorMode(RGB, 1)

  frustumProj = createMatrix(4)
  vBuf        = new Float32Array(16)
  eBuf        = new Float32Array(16)

  panel = createPanel({
    d:       { min: 0, max: 1, value: 0, step: 0.01 },
    animate: { value: true },
  }, {
    title: &amp;#39;NDC morph&amp;#39;, collapsible: true,
    labels: true, color: &amp;#39;white&amp;#39;,
    x: width - 130, y: 10,
  })

  // Morph body is identical for fill and stroke; only the struct type differs:
  //   fill   → &amp;#39;Vertex&amp;#39;        (struct: position, normal, texCoord, color)
  //   stroke → &amp;#39;StrokeVertex&amp;#39;  (struct: position, tangentIn, tangentOut, color, weight)
  const morphBody = `{
    vec4 fPos       = uViewFrustumMatrix * vec4(inputs.position, 1.0);
    vec2 xy         = -(fPos.xy / fPos.z) * (1.0 &amp;#43; ndc) * n;
    fPos.xy         = mix(fPos.xy, xy, d);
    inputs.position = (uEyeFrustumMatrix * fPos).xyz;
    return inputs;
  }`

  const morphUniforms = {
    uniforms: {
      &amp;#39;mat4 uViewFrustumMatrix&amp;#39;: () =&amp;gt; vBuf,
      &amp;#39;mat4 uEyeFrustumMatrix&amp;#39;:  () =&amp;gt; eBuf,
      &amp;#39;float d&amp;#39;:   () =&amp;gt; panel.d.value(),
      &amp;#39;float ndc&amp;#39;: () =&amp;gt; NDC,
      &amp;#39;float n&amp;#39;:   () =&amp;gt; N,
    },
  }

  fillMorph = buildColorShader({
    ...morphUniforms,
    &amp;#39;Vertex getWorldInputs&amp;#39;: `(Vertex inputs) ${morphBody}`,
  })

  outlineMorph = buildStrokeShader({
    ...morphUniforms,
    &amp;#39;StrokeVertex getWorldInputs&amp;#39;: `(StrokeVertex inputs) ${morphBody}`,
  })

  cajas = Array.from({ length: 40 }, () =&amp;gt; ({
    position: createVector(random(-50, 50), random(-50, 50), random(-25, 75)),
    size:     random(5, 20),
    col:      color(random(), random(), random()),
  }))

  mainCam = createCamera()
  mainCam.camera(
    -250.56, -257.35, 181.54,
    -249.9,  -256.75, 181.2,
    -0.57, 0.745, 0.34
  )
  frustumCam = createCamera()
  setCamera(mainCam)

  const f = 7 / 30
  ortho(-width * f, width * f, -height * f, height * f, 1, 10000)
}

// ── draw ──────────────────────────────────────────────────────────────────────
function draw() {
  background(&amp;#39;#138d75&amp;#39;)

  if (panel.animate.value()) {
    panel.d.set(map(sin(frameCount / 60), -1, 1, 0, 1))
  }

  updateFrustum()

  // grid
  push()
  rotateX(HALF_PI)
  strokeWeight(0.2)
  stroke(&amp;#39;#FFC300&amp;#39;)
  grid({ subdivisions: 20, size: 240 })
  pop()

  // single scene pass — fill, stroke, and frustum gizmo all morphed together
  push()
  shader(fillMorph)
  strokeShader(outlineMorph)
  strokeWeight(0.8)
  stroke(&amp;#39;magenta&amp;#39;)
  scene()
  fill(1, 0, 1, 0.3)
  viewFrustum({
    mat4Eye: eBuf,
    mat4Proj: frustumProj,
    bits: p5.Tree.NEAR | p5.Tree.FAR,
    viewer: () =&amp;gt; {
      push()
      resetShader()
      axes({ size: 50, bits: p5.Tree.X | p5.Tree._Y | p5.Tree._Z })
      pop()
    },
  })
  pop()
}

// ── updateFrustum ─────────────────────────────────────────────────────────────
function updateFrustum() {
  frustumCam.camera(0, 0, Z, ...p5.Tree.k, ...p5.Tree.j)
  frustumCam.mat4View(vBuf)
  frustumCam.mat4Eye(eBuf)
  const far = N * (1 &amp;#43; 2 * tan(FOVY / 2) * (1 &amp;#43; NDC))
  frustumProj.perspective(FOVY, 1, N, far)
}

// ── scene ─────────────────────────────────────────────────────────────────────
function scene() {
  cajas.forEach(({ position: pos, size, col }) =&amp;gt; {
    push()
    fill(col)
    translate(pos.x, pos.y, pos.z)
    box(size)
    pop()
  })
}

        &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;h1 id=&#34;shaders&#34;&gt;Shaders&lt;/h1&gt;
&lt;p&gt;The morph lives in a single GLSL hook injected into p5&amp;rsquo;s base shaders through &lt;a href=&#34;https://beta.p5js.org/reference/p5/buildColorShader/&#34;&gt;&lt;code&gt;buildColorShader&lt;/code&gt;&lt;/a&gt; and &lt;a href=&#34;https://beta.p5js.org/reference/p5/buildStrokeShader/&#34;&gt;&lt;code&gt;buildStrokeShader&lt;/code&gt;&lt;/a&gt; — no shader files needed.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Platonic Solids</title>
      <link>https://jpcharalambosh.co/posts/platonic/</link>
      <pubDate>Fri, 17 May 2024 15:32:05 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/platonic/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://en.wikipedia.org/wiki/Platonic_solid&#34;&gt;Platonic solids&lt;/a&gt;, named after the philosopher &lt;a href=&#34;https://en.wikipedia.org/wiki/Plato&#34;&gt;Plato&lt;/a&gt;, are highly symmetrical, three-dimensional shapes. Each face of a Platonic solid is the same regular polygon, and the same number of polygons meet at each vertex. This sketch demonstrates the rendering of Platonic solids using the &lt;a href=&#34;https://github.com/VisualComputing/p5.platonic&#34;&gt;p5.platonic&lt;/a&gt; library, showcasing both the immediate mode and retained mode rendering of the shapes. Check out the &lt;a href=&#34;https://jpcharalambosh.co/posts/platonic_cells/&#34;&gt;platonic cells&lt;/a&gt; example, which fills quadrille cells with the Platonic solids introduced here.&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
