Search code examples
javascriptgraphics3dp5.js

3D Object Faces Disappearing in p5.js


I am trying to make a 3D box with a pattern on each side using the following code but when viewed from certain angles, the back faces disappear when looking through the transparent parts of the forward faces. I was also wondering if it is possible to have a different pattern on each face? Many thanks in advance!

let r = 10
let a = 0
let c = 20
let angle = 0
let art

function setup() {
  createCanvas(windowWidth, windowHeight, WEBGL);
  art = createGraphics(800, 800)

}

function draw() {
  background(0);
  
  let x = r + c * cos(a)
  let y = r + c * sin(a)

  art.fill(r, a, c)
  art.ellipse(x + 400, y + 400, 10, 10)

  c += 0.2
  a += 1.8

  push()
  texture(art)
  rotateX(angle)
  rotateY(angle)
  rotateZ(angle)
  box(400)

  angle += 0.0003
  pop()

  orbitControl();
}
html, body { margin: 0; overflow: hidden; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>


Solution

  • This happens because in in WebGL, once a pixels is drawn, regardless of the level of level of transparency of that pixel, if another triangle would draw to that same pixel, but at a further depth, it is discarded (I think the alpha information from the original pixel(s) may no longer be available). In order for transparency to work properly in WebGL it is necessary to draw all triangles in depth order (furthest from the camera first). And even then if two triangles intersect there will still be problems.

    In your case because you have many pixels that are completely transparent and others that are completely opaque there is another solution: a custom fragment shader that discards pixels if the texture alpha is below some threshold.

    const vert = `
    uniform mat4 uModelViewMatrix;
    uniform mat4 uProjectionMatrix;
    
    attribute vec3 aPosition;
    attribute vec2 aTexCoord;
    
    varying vec2 vTexCoord;
    
    void main() {
      vTexCoord = aTexCoord;
    
      vec4 viewModelPosition = uModelViewMatrix * vec4(aPosition, 1.0);
      gl_Position = uProjectionMatrix * viewModelPosition;
    }`;
    
    const frag = `
    precision mediump float;
    
    // ranges from 0..1
    varying vec2 vTexCoord;
    
    uniform sampler2D uSampler;
    
    void main() {
      vec4 tex = texture2D(uSampler, vTexCoord);
      if (tex.a < 0.05) {
        discard;
      }
    
      gl_FragColor = tex;
    }`;
    
    let r = 10
    let a = 0
    let c = 20
    let angle = 0
    let art
    let discardShader;
    
    function setup() {
      createCanvas(windowWidth, windowHeight, WEBGL);
      art = createGraphics(800, 800)
      discardShader = createShader(vert, frag)
      textureMode(NORMAL)
    }
    
    function draw() {
      background(0);
    
      let x = r + c * cos(a)
      let y = r + c * sin(a)
    
      art.fill(r, a, c)
      art.ellipse(x + 400, y + 400, 10, 10)
    
      c += 0.2
      a += 1.8
    
      push()
      noStroke()
      texture(art)
      shader(discardShader)
      rotateX(angle)
      rotateY(angle)
      rotateZ(angle)
      box(400)
    
      angle += 0.0003
      pop()
    
      orbitControl();
    }
    html,
    body {
      margin: 0;
      overflow: hidden;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.1/p5.js"></script>

    Note #1: It is important that you use p5.js v1.4.1 for this to work because prior to that there was a bug that prevented user shaders from working with textures.

    Note #2: If your texture had partial opacity then this would not work and instead you would want to render each plane of the box separately and in the correct order (farthest from the camera first).