stride3d / stride

Stride Game Engine (formerly Xenko)
https://stride3d.net
MIT License
6.44k stars 930 forks source link

single-pass-wireframes in shader #698

Open jeske opened 4 years ago

jeske commented 4 years ago

This is an issue to track a feature I plan to add sometime soon.

The goal is to add Single-Pass Wireframe Rendering to Xenko's shaders, and use that for wireframes in the Studio, instead of 2-pass xray wireframes. Here is an example...

image

Why?

There are many advantages to single-pass wireframes.

The most immediate problem this will fix is the fact that the current x-ray wireframes make placing objects very very confusing. Take a look at two screenshots..

Here is one with a character standing on a surface.

image

Here is one with the character "submerged" up to the waist. The x-ray wireframes make this very hard to see.

image

I'm guessing the current wireframes are x-ray because non-xray 2-pass wireframes have many z-fighting issues, and also it may be advantageous to see the wireframes through objects.

My plan is to add single-pass wireframes to the shader, and add some features, such as:

Notes:

xen2 commented 4 years ago

It's true that this case is extremely confusing (good example with the character).

I agree that one-pass has some advantages which you nicely summarized, but it's definitely not a trivial fix.

Some of the issues I can think of why 1 pass is not trivial:

It has some other advantages though:

Anyway, this use case with the character is quite bothering, I will have to think if there's a way to fix it with current 2-pass approach (i.e. make the lower half 50% transparent where Z-test fail). There's probably enough flexibility in shader that we should be able to do something good enough. Otherwise I need to evaluate if 1-pass is possible (there's still the blockers such as see-through not possible anymore, post-effect, etc.)

xen2 commented 4 years ago

BTW, sorry I thought it was a feature request specific to Game Studio but I skimmed through the part where you said you need it for your own need!

By all means, go ahead, it could be useful for many other use cases than only the Game Studio (for example for in-game highlighting, etc.) And if it works well enough and we can figure out those potential issues I listed, we can see if it can be useful for the Game Studio as well!

xen2 commented 4 years ago

My plan is to add single-pass wireframes to the shader, and add some features, such as:

allowing occluded lines to be an alternate (muted) color, or culled contrast-coloring lines (so on yellow objects they will appear black)

How could you do display occluded line in a single pass while drawing the mesh itself? Doesn't it need a different depth stencil & blend states to have occluded-line being visible while keeping occluded part of the model invisible? I suppose there must be some kind of technique/trick, any link/doc? I am interested to read about that!

jeske commented 4 years ago

Here's the single-pass-wireframes doc link again. It's in the first post also.

I'd like to add the humerous observation that this issue is now 3x longer than the code for single-pass wireframes. :)

Doesn't it need a different depth stencil & blend states to have occluded-line being visible while keeping occluded part of the model invisible?

My previous single-pass-wireframe implementation didn't have this specific feature, but I don't see why it would.

SinglePass wireframes is a texel color shader, which computes the distance from the polygon edge in screen-space to compute a "wireframe line color weight", to perform an anti-aliased blend of the face texel color and edge texel color. By doing it all in one shader, the face color and edge color can be modified and blended in arbitrary ways, using the z-test or comparison with each other.

How could you display an occluded line in a single pass while drawing the mesh itself?

Normally, occluded texels are fast rejected with an early z-test, but they don't have to be. If one wants only occluded wireframe lines (not occluded polygon texels), the wireframe distance-to-edge calculation occurs first, and it only fast-fails the z-test if it's occluded and not on a wireframe line. If the texel lies on a wireframe edge, then it's a wireframe line.. then if the z-test passes (non-occluded), blend texel and edge color. if the z-test fails (occluded), just output the "occluded wireframe line color".

There are obviously more GPU branches here, but drawing polys is so much faster than line primitives that it's still faster. And performance usually isn't a priority for wire-renderings anyway, or nobody would use line-primitives.


Making this issue more clear, there are really two topics co-mingled here...

I assert that TriangleTexelWires are strictly better than LinePrimitiveWires . Before I implemented them myself once, I don't think I would have guessed it either, but TriangleTexelWires are massively faster, and are much more flexible - which means they can look better and have more features. I don't understand any situation where LinePrimitiveWires are better. They are still in the API because of ease and legacy compatibility.

In most cases, SinglePass rendering of the poly face and wire-edge colors is enough. In some special cases (like avoiding post filtering), maybe a second pass would be warranted, but I still argue the second pass should be TriangleTexelWires , not LinePrimitiveWires.

The actual object has backface culling and Z-depth, so we can't draw anything see-through (like backface wireframe). Need to test how it feels (i.e. if I select a cube, does it look better with backface see-through wireframe or not?)

Obviously if one wants backface wires, one has to turn off backface culling for that object draw. However, when shading a backface polygon, one doesn't have to output every texel, so it's easy to ignore occluded face texels but draw occluded wire-edge texels.

Because GPUs draw filled triangles faster than line primitives, this is generally faster than drawing LINES anyway, and gives more flexibility and looks better. I've never done it, but it should also be easy and fast to draw backface lines in a different color, if that is desired.

From a UI perspective, I find backface wires (along with all x-ray drawing) extremely confusing to look at in all but the simplest models. And few game models are simple. They interfere with the ability to perceive model shape and depth.

I also find wireframe object highlighting to be highly chaotic, especially on high poly models. I personally prefer outline highlight, like Blender (see pic). Maybe some math to vary edge intensity (or skip edges) by edge angle could be an alternative.

image

We render wireframe after post-effects to have always the same color (and also we don't want it blurred or affected by depth of field).

This is a tricky issue i've not considered before. Here are a few brainstorm ideas:

xen2 commented 4 years ago

Thanks for the detailed write-up and sorry for missing the link in the original post...

I am totally with you on switching to a triangle-based approach for rendering (aliasing, better rendering control & performance). We already do this kind of thing for rendering the unit grid and it looks great! This step should be quite easy to do even now, it's just a minor change.

I wouldn't be concerned about Z-fighting: rendering is supposed to be deterministic so Z-fighting should be a non-issue. Otherwise, any kind of Z-prepass wouldn't work. We can rediscuss 1-pass vs 2-pass once the rendering has been changed to triangle and looks good.

Another topic: if we want to keep displaying occluded part but in a more subtle way, here's one way we could do it (at the cost of an extra draw call):

Also, we currently display backfaces of wireframe but I wouldn't mind disabling that. For me, the important part of making visible occluded wireframe is more the case where it's behind another object/wall.

jeske commented 4 years ago

I just realized some of these fancy ideas like displaying occluded parts differently (in any way) introduces order-dependence, like order-dependent alpha passes.

I think this all could be nicely fixed by keeping the 2-pass technique after screen-space filters... and switching it to a triangle based approach. That also has the advantage that it becomes a separate wireframe shader, rather than bloating the main shader.

I'll think on this some more, but I think this approach sounds pretty flexible and nice.

jeske commented 4 years ago

I want to also add this example of what Blender's outline shader looks like when an object is occluded. It still draws the occluded portion of the outline, and the effect works pretty well.

image