Internal Faces in PyPlasm-Generated Tube Geometry

Background

I’m working with complex tube geometry generated using PyPlasm (a powerful geometry library) and rendering it in Three.js. The geometry extraction works flawlessly, but I’m facing a challenging transparency issue that I can’t seem to resolve elegantly.

The Problem

When applying opacity to my tube object, internal faces become visible, creating an unwanted “see-through” effect that shows the tube’s internal structure rather than a clean, hollow appearance.

Here’s what I’m seeing:

  • Solid rendering: Perfect - clean tube appearance :white_check_mark:
  • With transparency: Internal faces visible - looks broken :cross_mark:

What I’ve Tried

Standard Approaches (Didn’t Work)

javascript

// Attempted various material settings
const material = new THREE.MeshPhongMaterial({ // tried other materials too
  transparent: true,
  opacity: 0.6,
  side: THREE.FrontSide,     // Tried DoubleSide too
  depthWrite: true,          // Tried false
  depthTest: true            // Tried false
});

Depth Pre-Pass Technique (Partially Works)

I implemented a two-pass rendering approach:

javascript

// Pass 1: Depth Pass - Write depth only
const depthMaterial = new THREE.MeshBasicMaterial({
  colorWrite: false,    // Don't write color
  depthWrite: true,     // Write to depth buffer
  depthTest: true
});

// Pass 2: Color Pass - Render with transparency
const colorMaterial = new THREE.MeshLambertMaterial({
  transparent: true,
  opacity: 0.6,
  depthWrite: false,    // Don't write depth (use existing)
  depthTest: true       // Test against existing depth
});

Result: This works great! The tube renders cleanly with no internal faces visible.

The New Problem

However, this solution breaks down when I need to place objects inside the tube:

  1. Interior objects need depthWrite: false and depthTest: false to render inside the tube
  2. Nested transparency (tube inside tube) fails because I’d need multiple depth passes
  3. Complex material management - tracking which objects are “inside” vs “outside”

The Question

Is there a way to achieve clean transparency with PyPlasm-generated geometry without:

  • Multiple rendering passes
  • Complex material hierarchies
  • Manual depth management for nested objects

Specific questions:

  1. Could this be a geometry issue from PyPlasm’s boundary extraction?
  2. Are there Three.js transparency techniques I’m missing?
  3. Should I pre-process the geometry to remove internal faces before sending to Three.js?
  4. Is there a shader-based solution that could handle this elegantly?

Geometry Details

  • Generated using PyPlasm’s BOUNDARY() or Plasm.getBatches()
  • Exported as vertex/normal arrays to Three.js
  • Complex hollow tube with proper normals
  • Works perfectly in solid rendering mode

Any insights or alternative approaches would be greatly appreciated! I’m open to solutions at any level - geometry preprocessing, Three.js techniques, or shader approaches.

Tube with wireframe:

main.js

import * as THREE from 'three';
import * as input_data from './clean_triangle_data.json';
import { VertexNormalsHelper } from 'three/examples/jsm/helpers/VertexNormalsHelper.js';

const scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);
scene.add(new THREE.AxesHelper(1));

const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.01,
  100
);
camera.position.z = 13;

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.sortObjects = true;
document.body.appendChild(renderer.domElement);

let isDragging = false;
let previousMousePosition = { x: 0, y: 0 };

renderer.domElement.addEventListener('mousedown', () => isDragging = true);
renderer.domElement.addEventListener('mouseup', () => isDragging = false);
renderer.domElement.addEventListener('mousemove', (e) => {
  if (isDragging) {
    customObject.rotation.y += (e.clientX - previousMousePosition.x) * 0.01;
    customObject.rotation.x += (e.clientY - previousMousePosition.y) * 0.01;
  }
  previousMousePosition = { x: e.clientX, y: e.clientY };
});

const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);

const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
dirLight.position.set(1, 1, 1).normalize();
scene.add(dirLight);

// Create geometry using BOTH vertices AND faces
const geometry = new THREE.BufferGeometry();

// Get vertices and faces from boundary data
const vertices = new Float32Array(input_data.vertices);
const normals = new Float32Array(input_data.normals);

console.log('Vertices:', vertices.length);
console.log('normals:', normals.length);

geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
geometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3));

//geometry.computeVertexNormals();
//geometry.computeBoundingSphere();
geometry.computeBoundingBox();

const material = new THREE.MeshBasicMaterial({
  color: 0x444444,
  transparent: true,
  opacity: 0.6,
  side: THREE.DoubleSide,
  depthWrite: true,
  depthTest: false,
});

const mesh = new THREE.Mesh(geometry, material);

const customObject = new THREE.Group();
customObject.add(mesh);

// Now add objects inside - should work without transparency issues!
const cylinderGeometry = new THREE.CylinderGeometry(0.5, 0.5, 4, 16);
const insideObjectMaterial = new THREE.MeshPhongMaterial({
  color: 0xff0000,
  transparent: true,
  opacity: 0.8,
});

const insideCylinder = new THREE.Mesh(cylinderGeometry, insideObjectMaterial);
insideCylinder.position.set(0, 0, 2.5);
insideCylinder.rotation.x = Math.PI / 2;

//customObject.add(insideCylinder); // Enable this line now!

scene.add(customObject);

function animate() {
  renderer.clear();
  //customObject.rotation.x += 0.01;
  //customObject.rotation.y += 0.01;
  renderer.render(scene, camera);
}

renderer.setAnimationLoop(animate);

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});```

`clean_triangle_data.json`
[clean_triangle_data.json|attachment](upload://nNfZn04t4Sy24mP7PoRi73GRpGS.json) (74.7 KB)

I would go back to the source. A geometry generator ideally should not generate interior faces like that.
Maybe there is some flag that prevents that? Seems like it will continue to cause issues, and it’s also not very inefficient rendering-wise.
There’s no easy way to get rid of interior faces like that afaik.

There is TubeGeometry three.js docs

but seems to only be single walled…

There is also ExtrudeGeometry that could work for this (you may have to pass the interior circle as a “hole” in the path:

3 Likes

The Maestro solved it:

3 Likes

Another approach for hollow cylinders: Tube in a path, made with CSG subtraction of cylinders - #6 by prisoner849

1 Like