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
- With transparency: Internal faces visible - looks broken
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:
- Interior objects need
depthWrite: false
anddepthTest: false
to render inside the tube - Nested transparency (tube inside tube) fails because I’d need multiple depth passes
- 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:
- Could this be a geometry issue from PyPlasm’s boundary extraction?
- Are there Three.js transparency techniques I’m missing?
- Should I pre-process the geometry to remove internal faces before sending to Three.js?
- Is there a shader-based solution that could handle this elegantly?
Geometry Details
- Generated using PyPlasm’s
BOUNDARY()
orPlasm.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)