Hi everyone,
I've been building a suspension design tool on top of Chrono + VSG and ran into some transparency rendering issues that I traced back to a small bug in `chrono_vsg`. I've put together a fix and wanted to share it with the community in case others have hit the same problem.
**The issue**
When you set `opacity < 1.0` on a `ChVisualMaterial` and render through `ChVisualSystemVSG`, three things go wrong:
1. **One-sided transparency** — the semi-transparent object looks opaque from one direction. Rotate the camera 180 degrees and suddenly you can see through it. This is because backface culling is still active on the transparent pipeline.
2. **Objects disappear behind transparent surfaces** — if you have geometry behind a transparent body (e.g. hardpoints or linkages visible through a semi-transparent tire), it simply vanishes. The transparent surface writes to the depth buffer and occludes everything behind it.
3. **Opaque objects rendered double-sided** — less obvious, but all opaque geometry ends up with `VK_CULL_MODE_NONE`, which wastes fill rate and can cause z-fighting on thin-walled parts.
**Root cause**
The bug is in `ShaderUtils.cpp`, in the `SetPipelineStates` visitor inside `createPbrStateGroup()` (around line 440). There are two issues:
First, the cull-mode condition is inverted. The code reads:
```cpp
void apply(vsg::RasterizationState& rs) {
if (!blending) {
// combination of color blending and two sided lighting leads to strange effects
rs.cullMode = VK_CULL_MODE_NONE;
}
...
}
```
The comment shows the intent was to handle blending, but `!blending` means this fires for *opaque* objects. Transparent objects keep the default `VK_CULL_MODE_BACK_BIT` and only show one face.
Second, the visitor never touches `DepthStencilState`. Transparent surfaces default to `depthWriteEnable = VK_TRUE`, so they write into the Z-buffer and hide everything behind them.
**The fix**
Two changes to the `SetPipelineStates` visitor:
1. Flip the condition: `if (!blending)` → `if (blending)` for the cullMode override
2. Add a `DepthStencilState` override to disable depth writes for transparent pipelines:
```cpp
void apply(vsg::RasterizationState& rs) {
if (blending) {
// Transparent objects need both faces visible
rs.cullMode = VK_CULL_MODE_NONE;
}
if (wireframe)
rs.polygonMode = VK_POLYGON_MODE_LINE;
else
rs.polygonMode = VK_POLYGON_MODE_FILL;
}
void apply(vsg::DepthStencilState& dss) {
if (blending) {
// Don't write to depth buffer — let geometry behind show through
dss.depthWriteEnable = VK_FALSE;
}
}
```
That's it — the rest of the visitor stays the same.
**Results**
Before the fix, a semi-transparent tire with hardpoints behind it:
- Tire appears opaque from the back
- Hardpoints and A-arms invisible through the tire
- Opaque spring bodies show occasional z-fighting

After the fix:
- Transparency is consistent from all viewing angles
- Internal geometry (hardpoints, links) is clearly visible through transparent surfaces
- Opaque objects render correctly with proper backface culling

**Workaround for pre-compiled Chrono**
If you're linking against a pre-built Chrono library and can't patch the source, you can apply a post-`BindAll()` scene graph traversal that finds `BindGraphicsPipeline` nodes, checks `ColorBlendState` attachments for `blendEnable == VK_TRUE`, and patches the `DepthStencilState` and `RasterizationState` on the fly. Happy to share the code if anyone needs it.
I've also filed this as a GitHub issue
https://github.com/projectchrono/chrono/issues/698 with a proposed patch. Would be great to get this into a future release so everyone benefits.
Cheers,
Tudor