This Rosetta demo is intended to give readers a starting point for porting the Brush VR app to three.js and WebGL2, highlighting some of the lower-level aspects of managing shaders.

code
let color, depth, brush, escorzo = true, fallback = [], points = [], record
let basicShader

function preload() {
  loadJSON('/cloud_500.json', json =>
    fallback = json.map(entry => ({
      worldPosition: createVector(entry.x, entry.y, entry.z),
      color: entry.color
    }))
  )
}

function setup() {
  createCanvas(600, 400, WEBGL)
  colorMode(RGB, 1)
  document.oncontextmenu = () => false
  points = [...fallback]
  const o = parsePosition(Tree.ORIGIN, { from: Tree.WORLD, to: Tree.SCREEN })
  depth = createSlider(0, 1, o.z, 0.001)
  depth.position(10, 10)
  depth.style('width', '580px')
  color = createColorPicker('#C7C08D')
  color.position(width - 70, 40)
  brush = sphereBrush
  basicShader = parseShader(`#version 300 es
   precision highp float;
   uniform vec4 uMaterialColor;
   out vec4 fragColor;
   void main() {
     fragColor = uMaterialColor;
   }`, Tree.pMatrix | Tree.vMatrix | Tree.mMatrix)
}

function draw() {
  (mouseY >= 30) && orbitControl()
  shader(basicShader)
  record && update()
  background('#222226')
  axes({ size: 50, bits: Tree.X | Tree.Y | Tree.Z })
  for (const point of points) {
    push()
    translate(point.worldPosition)
    brush(point)
    pop()
  }
}

function keyPressed() {
  key === 'c' && (points = [])
  key === 'f' && focus()
  key === 'r' && (record = !record)
  key === 's' && saveCloud()
}

function update() {
  points.push({
    worldPosition: parsePosition([mouseX, mouseY, depth.value()], { from: Tree.SCREEN, to: Tree.WORLD }),
    color: color.color(),
  })
}

function focus() {
  const center = [0, 0, 0]
  const position = parsePosition()
  const up = parseDirection(Tree.j)
  camera(...position.array(), ...center, ...up.array())
  const o = parsePosition(Tree.ORIGIN, { from: Tree.WORLD, to: Tree.SCREEN })
  depth.value(o.z)
}

function sphereBrush(point) {
  push()
  noStroke()
  fill(point.color)
  sphere(1)
  pop()
}

function saveCloud() {
  const data = points.map(point => ({
    x: point.worldPosition.x,
    y: point.worldPosition.y,
    z: point.worldPosition.z,
    color: [red(point.color) / 255, green(point.color) / 255, blue(point.color) / 255, alpha(point.color) / 255]
  }))
  saveJSON(data, 'custom_cloud.json')
}

Shader Declaration

A key difference in this version is the use of parseShader, which defines a basic shader that handles color through the uMaterialColor uniform:

basicShader = parseShader(`#version 300 es
   precision highp float;
   uniform vec4 uMaterialColor;
   out vec4 fragColor;
   void main() {
     fragColor = uMaterialColor;
   }`, Tree.pMatrix | Tree.vMatrix | Tree.mMatrix)

Observations

  1. The vertex shader generated by parseShader is logged to the console. It is as follows:
    #version 300 es
    precision highp float;
    in vec4 aPosition;
    uniform mat4 uViewMatrix;
    uniform mat4 uModelMatrix;
    uniform mat4 uProjectionMatrix;
    void main() {
      gl_Position = uProjectionMatrix * uViewMatrix * uModelMatrix * aPosition;
    }
    
  2. p5.js emits:
    • Transformation matrices from orbitControl.
    • Geometry from sphere.
    • Color data from the color() function.
  3. In both three.js and WebGL2, shaders must be explicitly declared. While three.js simplifies some of the process, both frameworks require managing shaders within the rendering pipeline. It is necessary to determine how geometry, uniform matrices, and color data are passed from the JavaScript program to the shaders.

Porting to Three.js and WebGL2

Three.js simplifies 3D development by abstracting the complexities of WebGL, providing built-in geometry, material, and scene management tools. It also handles features like OrbitControls to control camera movement.

WebGL2, on the other hand, provides more direct access to the GPU, requiring manual management of buffers and shaders. For instance, when porting this app, you will need to manually create and bind Vertex Buffer Objects (VBOs) and set view matrix uniforms instead of relying on functions like orbitControl() or sphere() in p5.js.

This exercise aims to close the cognitive gap between high-level p5.js abstractions and lower-level WebGL programming.