I think it is related to how the caps are added.
i wonder why are there these duplicate inputs on the node? except for shading color, that is not even defined in the shader and does nothing.
I have collected all the fixes and improvements so far and tidied up the code a bit + GPT wrote a smooth normals lerp.
-Vertexcount lowered to 56 (to support texture)
-Toggle for full/per segment texture coordinates
-Global transform working
-GS instancing tebjan, no inputs for it though. (See line 41 and 83)
-Numsides input (max 8, max 14 without caps. anything higher will drop vertices)
-Caps generated with AppendVertex()
-Smooth normals lerp (0-1)
-Caps toggle
Tubby.zip (13.9 KB)
Wonderful! Great work @tgd
Some details that I really don’t know if are real problems or not.
First, if you set Num Sides to eg. 3, it has a weird behavior at the ends, if it is curled up like in the example:
Second, Beginning from 15 sides, the tube is being “Sliced up”
And I have one suggestion - an option to disable the end caps, if they are not needed.
Id say great work @mburk !
Also noticed the end cap not matching the tube when numsides is an odd number. Looked at that part of the code but did not find out, the math is not easy.
The slicing is due to the GS vertex limit. The calculations get done but the additional vertices are not drawn.
Will add a toggle for the caps, thats easy. They are already left out when the tube is closed.
Just another thought, No idea if it makes sense in this contribution, but it would be a cool way to make “snakes” with a tail, that starts big and the other end is very pointy.
So having a start en end thickness would be cool.
Uh, I missed the conversation.
Love, how this turned into a community effort!
And thanks @tgd!
@sunep thickness buffer should definitely be added.
oh, I didn’t check, does the texture work on the inside?
This could be used to make a super nice version of the Arkaos tunnel from 98 or so… If you guys remember that one.
Now with size buffer.
THe caps generation is not correct in this version, will need to fix. The “non optimized” version was correct. This version has flipped and redundand triangles.
@mburk I found the shader that has shared vertices for the smooth normals case and doesn’t emit faces not seen by the camera for flat normals, which allows for higher resolution, maybe you can take some bits from it:
void FlatNormalTube(float3 startPosition, float3 endPosition, float4 startColor, float4 endColor, float3 right, float3 up, uint segmentCount, inout TriangleStream<Output> triangleStream)
{
float angle = 0;
float angleStep = 2 * PI / segmentCount;
// Camera position in world space
float3 Eye = mul(float4(0, 0, 0, 1), ViewInverse).xyz;
// tube faces with flat normals
for (uint i = 0; i < segmentCount; i++)
{
float angle1 = angle + i * angleStep;
float angle2 = angle + (i + 1) * angleStep;
float3 pos1 = startPosition + right * cos(angle1) + up * sin(angle1);
float3 pos2 = startPosition + right * cos(angle2) + up * sin(angle2);
float3 pos3 = endPosition + right * cos(angle2) + up * sin(angle2);
float3 pos4 = endPosition + right * cos(angle1) + up * sin(angle1);
// Calculate the center point of the face
float3 center = (pos1 + pos2 + pos3 + pos4) / 4;
// get the view direction in world space
float3 viewDir = Eye - center;
// Calculate face normal based on 3 points
float3 faceNormal = normalize(cross(pos2 - pos1, pos3 - pos1));
// Check if the face is facing the camera
if (dot(viewDir, faceNormal) < 0)
{
continue;
}
float2 texcoord1 = float2(i / float(segmentCount), 0);
float2 texcoord2 = float2((i + 1) / float(segmentCount), 0);
float4 col = startColor;
EmitVertexWS(pos2, faceNormal, texcoord2, col, triangleStream);
EmitVertexWS(pos1, faceNormal, texcoord1, col, triangleStream);
texcoord1 = float2((i + 1) / float(segmentCount), 1);
texcoord2 = float2(i / float(segmentCount), 1);
col = endColor;
EmitVertexWS(pos3, faceNormal, texcoord2, col, triangleStream);
EmitVertexWS(pos4, faceNormal, texcoord1, col, triangleStream);
triangleStream.RestartStrip();
}
}
void SmoothNormalTube(float3 startPosition, float3 endPosition, float4 startColor, float4 endColor, float3 right, float3 up, uint segmentCount, inout TriangleStream<Output> triangleStream)
{
float angle = 0;
float angleStep = 2 * PI / segmentCount;
// First two vertices are the start and end points
float3 pos1 = startPosition + right;
float3 pos2 = endPosition + right;
float3 normal1 = normalize(pos1 - startPosition);
float3 normal2 = normalize(pos2 - endPosition);
float2 texcoord1 = float2(0, 0);
float2 texcoord2 = float2(0, 1);
EmitVertexWS(pos1, normal1, texcoord1, startColor, triangleStream);
EmitVertexWS(pos2, normal2, texcoord2, endColor, triangleStream);
// additional vertices per face
for (uint i = 0; i < segmentCount; i++)
{
angle += angleStep;
float3 pos1 = startPosition + right * cos(angle) + up * sin(angle);
float3 pos2 = endPosition + right * cos(angle) + up * sin(angle);
// Calculate face normal based on 3 points
//float3 faceNormal = normalize(cross(pos2 - pos1, pos3 - pos1));
// Calculate smooth normals based on the directions from the start and end points
float3 normal1 = normalize(pos1 - startPosition);
float3 normal2 = normalize(pos2 - endPosition);
float2 texcoord1 = float2(i / float(segmentCount), 0);
float2 texcoord2 = float2((i + 1) / float(segmentCount), 0);
float4 col = startColor;
EmitVertexWS(pos1, normal1, texcoord1, startColor, triangleStream);
EmitVertexWS(pos2, normal2, texcoord2, endColor, triangleStream);
}
triangleStream.RestartStrip();
}
[maxvertexcount(46)]
stage void GSMain(point Input input[1], inout TriangleStream<Output> triangleStream)
{
streams = input[0];
float3 startPosition;
float3 endPosition;
float3 tubeDir;
float4 startColor;
float4 endColor;
if (!SplinesCondition(startPosition, endPosition, tubeDir, startColor, endColor))
{
return;
}
float3 axis = tubeDir;
float3 up = abs(axis.y) < 0.999 ? float3(0, 1, 0) : float3(1, 0, 0);
float3 right = normalize(cross(up, axis));
up = cross(axis, right);
//normalize up and right
right = normalize(right);
up = normalize(up);
//scale up and right according to size
right *= streams.PSize;
up *= streams.PSize;
uint segmentCount = max(3, SegmentCount);
if (FlatNormals)
FlatNormalTube(startPosition, endPosition, startColor, endColor, right, up, segmentCount, triangleStream);
else
SmoothNormalTube(startPosition, endPosition, startColor, endColor, right, up, segmentCount, triangleStream);
// // Add caps at both ends of the tube
// float3 capNormal = tubeDir; // Normal direction for the caps
// float angle = 0;
// float angleStep = 2 * PI / segmentCount;
// // Initialize the first vertex position
// float3 pos1 = startPosition + right * cos(angle) + up * sin(angle);
// // Emit the first vertex outside the loop
// EmitVertexWS(pos1, capNormal, float2(0, 0), startColor, triangleStream);
// for (uint i = 0; i <= segmentCount; i++)
// {
// // Calculate angles for the current segment
// float angle1 = angle + i * angleStep;
// // Calculate position for the next vertex
// float3 pos2 = startPosition + right * cos(angle1) + up * sin(angle1);
// // Emit the vertex for the current strip
// EmitVertexWS(pos2, capNormal, float2(i / float(segmentCount), 1), startColor, triangleStream);
// // Emit a degenerate vertex to connect strips (except for the last iteration)
// if (i < segmentCount)
// {
// EmitVertexWS(pos1, capNormal, float2(0, 0), startColor, triangleStream);
// }
// // Update the current position for the next iteration
// //pos1 = pos2;
// }
// // End the strip
// triangleStream.RestartStrip();
// // Add the bottom cap (similar to the top cap)
// // Calculate the normal direction for the bottom cap (opposite direction)
// float3 bottomCapNormal = -tubeDir;
// // Initialize the first vertex position (same as the top cap's last vertex)
// float3 pos1Bottom = endPosition + right * cos(angle) + up * sin(angle);
// // Emit the first vertex of the bottom cap
// EmitVertexWS(pos1Bottom, bottomCapNormal, float2(0, 0), endColor, triangleStream);
// for (uint i = 0; i <= segmentCount; i++)
// {
// // Calculate angles for the current segment
// float angle1 = angle + i * angleStep;
// // Calculate position for the next vertex
// float3 pos2Bottom = endPosition + right * cos(angle1) + up * sin(angle1);
// // Emit the vertex for the bottom cap
// EmitVertexWS(pos2Bottom, bottomCapNormal, float2(i / float(segmentCount), 1), endColor, triangleStream);
// // Emit a degenerate vertex to connect strips (except for the last iteration)
// if (i < segmentCount)
// {
// EmitVertexWS(pos1Bottom, bottomCapNormal, float2(0, 0), endColor, triangleStream);
// }
// // Update the current position for the next iteration
// pos1Bottom = pos2Bottom;
// }
// // End the strip for the bottom cap
// triangleStream.RestartStrip();
}
what i also noticed is the shading does not change with world transform. like when you rotate the tube upside down, the lighting/reflections seem to stick.
it seems to be fixed by multiplying streams.normalWS with world, like the position. not sure if this is the correct way to do though. there is also streams.meshNormalWS but cant tell if it needs that too, cant see a difference.
You should apply the world matrix to the start/end points before the tube is built.
Couldn’t this mess up shadows? Faces not visible to the camera might still be relevant for the shadow projection, or is Stride evaluating the shadow calculation basically with its own camera and therefore creating its own mesh for the shadow pass?
Yes, the shadow pass has the camera view of the light source. So it should work fine.
It’s only an issue if the mesh would be generated before it’s rendered.
Performance wise, i haven’t tested which is better.
There actually seem to be some issues with shadow mapping, at least with cascades of directional lights. but working otherwise.
I was able to use the GS Instancing to use one instance for caps, and another instance for the tube, which greatly increases the maximum segment count. looking into dynamically distributing tube generation over even more instances…
@tonfilm I just remembered the “energy-lines” in the aes park project. How were they done?
We had a custom Stride version by Tebjan running. It contained an experimental node called “Tubemesh” which generated a mesh from a given set of 3d vector points. We would also be happy to have that running outside its project context here. @tebjan: any chance you can extract it?



