-
Notifications
You must be signed in to change notification settings - Fork 434
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add DebugLineRender utility #1349
Add DebugLineRender utility #1349
Conversation
// thick, visually-appealing line. Todo: implement thick lines using | ||
// triangles, for example https://mattdesl.svbtle.com/drawing-lines-is-hard. | ||
// 1.2 is hand-tuned for Nvidia hardware to be just small enough so we don't | ||
// see gaps between the individual offset lines. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be clear: the current thick-line implementation here is sloppy in the sense of not producing perfectly-rendered thick lines. We can upgrade in the future without changing the public API, if we wish.
// restore blending state if necessary | ||
if (doToggleBlend) { | ||
Mn::GL::Renderer::disable(Mn::GL::Renderer::Feature::Blending); | ||
// Note: because we are disabling blending, we don't need to restore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mosra any concern here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as in the other comment, this should be something Magnum eventually takes care of.
For now, this is fine.
"DebugLineRender::flushLines: no GL resources; see " | ||
"also releaseGLResources", ); | ||
|
||
bool doToggleBlend = !glIsEnabled(GL_BLEND); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah sorry, a nasty pain point.
This should ideally eventually be handled by a scoped renderer state, where you enable what you need and Magnum's state tracker then takes care of cleaning that up again at the end of scope, ideally also not calling into GL more than necessary. Practically we'll probably switch to Vulkan sooner than I get to implement such a feature.
src/esp/gfx/DebugLineRender.cpp
Outdated
// Update shader | ||
_glResources->mesh.setCount(_verts.size()); | ||
|
||
glDisable(GL_LINE_SMOOTH); // anti-aliased lines cause artifacts |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feels weird calling OpenGL directly here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought this was some ancient relic and so this isn't even exposed anywhere.
If you want a "less" direct way and something that should be more futureproof, GL::Renderer::disable(GL::Renderer::Feature(GL_LINE_SMOOTH))
and then please add a TODO for me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also hit an issue with this not even compiling for Emscripten. I'm starting to think it's pretty unlikely that anyone is going to set this setting to true. I'm just going to remove this line and not worry about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, that's why I thought it's an ancient relic, because GLES/WebGL doesn't even define such a thing :D
} | ||
|
||
// return false if segment is entirely clipped | ||
bool scissorSegmentToOutsideCircle(Mn::Vector3* pt0, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is used for drawPathWithEndpointCircles
as seen in the video.
// Python may keep around other shared_ptrs to this object, but we need | ||
// to release GL resources here. | ||
debugLineRender_->releaseGLResources(); | ||
debugLineRender_ = nullptr; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is weird. What I really want to do is delete the DebugLineRender instance here. But I can't because I had to share shared_ptr
s with Python when doing the bindings. And Python tends to keep around the pointers indefinitely. So we don't actually control when this object gets deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We used a weak_ptr wrapper paradigm to get around this with ManagedObjects. The wrapper does a lock and forwards the command if it succeeds, otherwise warning about the stale reference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The weak_ptr
solution is interesting but feels like overkill for my situation. If a Python user keeps a reference to DebugLineRender after the sim has been closed, and she tries to use it, nothing bad happens.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall looks good. Cool feature.
src/esp/bindings/GfxBindings.cpp
Outdated
py::class_<DebugLineRender, std::shared_ptr<DebugLineRender>>( | ||
m, "DebugLineRender") | ||
.def("set_line_width", &DebugLineRender::setLineWidth, | ||
R"(See push_transform.)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update this doc? I assume it applied to lines following the command?
src/esp/bindings/GfxBindings.cpp
Outdated
const Magnum::Color4&, const Magnum::Color4&>( | ||
&DebugLineRender::drawTransformedLine), | ||
"from"_a, "to"_a, "from_color"_a, "to_color"_a, | ||
R"(Draw a line segment in world-space or local-space (see pushTransform).)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
R"(Draw a line segment in world-space or local-space (see pushTransform).)") | |
R"(Draw a line segment in world-space or local-space (see pushTransform) with interpolated color.)") |
src/esp/gfx/DebugLineRender.h
Outdated
/** | ||
* @brief Submit lines to the GL renderer. Call this once per frame. | ||
* Because this uses transparency, you should ideally call this *after* | ||
* submitting opaque scene geomtry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* submitting opaque scene geomtry. | |
* submitting opaque scene geometry. |
src/esp/gfx/DebugLineRender.cpp
Outdated
// Because we render lines multiple times additively in flushLines, we need to | ||
// remap alpha (opacity). This is an approximation. | ||
constexpr float exponent = 2.f; | ||
return Mn::Color4(src.r(), src.g(), src.b(), std::pow(src.a(), exponent)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code golf:
return Mn::Color4(src.r(), src.g(), src.b(), std::pow(src.a(), exponent)); | |
return {src.rgb(), std::pow(src.a(), exponent)}; |
src/esp/gfx/DebugLineRender.cpp
Outdated
if (dist1 < radius) { | ||
return false; | ||
} | ||
const float lerpFraction = (radius - dist0) / (dist1 - dist0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might want to look at Math::lerpInverted() :)
src/esp/gfx/DebugLineRender.cpp
Outdated
|
||
glDisable(GL_LINE_SMOOTH); // anti-aliased lines cause artifacts | ||
|
||
glLineWidth(_internalLineWidth); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GL::Renderer::setLineWidth(), but note that line widths larger than 1 are "not core GL" and thus you will only get this on NVidia, which has this only because CAD vendors refuse to update their ancient GL code.
(Yes, yes, I need to add a wide line renderer to Magnum, this is ridiculous.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, this feature is working fine on my Macbook with AMD Radeon Pro 5500M.
(Yes, yes, I need to add a wide line renderer to Magnum, this is ridiculous.)
👍
src/esp/gfx/DebugLineRender.cpp
Outdated
const float x = _internalLineWidth * 1.2f; | ||
constexpr float sqrtOfTwo = 1.4142f; | ||
// hard-coding 8 points around a circle | ||
const std::vector<Mn::Vector3> offsets = {Mn::Vector3(x, x, 0), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const std::vector<Mn::Vector3> offsets = {Mn::Vector3(x, x, 0), | |
constexpr Mn::Vector3 offsets[] = {Mn::Vector3(x, x, 0), |
No need for an allocation every time. Range-for works with C arrays as well.
src/esp/gfx/DebugLineRender.cpp
Outdated
// hard-coding 8 points around a circle | ||
const std::vector<Mn::Vector3> offsets = {Mn::Vector3(x, x, 0), | ||
Mn::Vector3(-x, x, 0), | ||
Mn::Vector3(x, -x, 0), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw., no need for typing that much:
Mn::Vector3(x, -x, 0), | |
{x, -x, 0}, |
src/esp/gfx/DebugLineRender.cpp
Outdated
// perf todo: do a custom shader constant for opacity instead so we don't have | ||
// to touch all the verts |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use Shaders::FlatGL3D
with Flag::VertexColor
enabled and call setColor({1.0f, 1.0f, 1.0f, opacity)
to get this effect.
The VertexColorGL3D
shader is there mainly because it's dead-simple and thus suitable for learning purposes.
src/esp/gfx/DebugLineRender.cpp
Outdated
} | ||
|
||
// restore to a reasonable default | ||
glLineWidth(1.0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GL::Renderer::setLineWidth()
again
void DebugLineRender::drawBox(const Magnum::Vector3& min, | ||
const Magnum::Vector3& max, | ||
const Magnum::Color4& color) { | ||
// 4 lines along x axis | ||
drawTransformedLine(Mn::Vector3(min.x(), min.y(), min.z()), | ||
Mn::Vector3(max.x(), min.y(), min.z()), color); | ||
drawTransformedLine(Mn::Vector3(min.x(), min.y(), max.z()), | ||
Mn::Vector3(max.x(), min.y(), max.z()), color); | ||
drawTransformedLine(Mn::Vector3(min.x(), max.y(), min.z()), | ||
Mn::Vector3(max.x(), max.y(), min.z()), color); | ||
drawTransformedLine(Mn::Vector3(min.x(), max.y(), max.z()), | ||
Mn::Vector3(max.x(), max.y(), max.z()), color); | ||
|
||
// 4 lines along y axis | ||
drawTransformedLine(Mn::Vector3(min.x(), min.y(), min.z()), | ||
Mn::Vector3(min.x(), max.y(), min.z()), color); | ||
drawTransformedLine(Mn::Vector3(max.x(), min.y(), min.z()), | ||
Mn::Vector3(max.x(), max.y(), min.z()), color); | ||
drawTransformedLine(Mn::Vector3(min.x(), min.y(), max.z()), | ||
Mn::Vector3(min.x(), max.y(), max.z()), color); | ||
drawTransformedLine(Mn::Vector3(max.x(), min.y(), max.z()), | ||
Mn::Vector3(max.x(), max.y(), max.z()), color); | ||
|
||
// 4 lines along z axis | ||
drawTransformedLine(Mn::Vector3(min.x(), min.y(), min.z()), | ||
Mn::Vector3(min.x(), min.y(), max.z()), color); | ||
drawTransformedLine(Mn::Vector3(max.x(), min.y(), min.z()), | ||
Mn::Vector3(max.x(), min.y(), max.z()), color); | ||
drawTransformedLine(Mn::Vector3(min.x(), max.y(), min.z()), | ||
Mn::Vector3(min.x(), max.y(), max.z()), color); | ||
drawTransformedLine(Mn::Vector3(max.x(), max.y(), min.z()), | ||
Mn::Vector3(max.x(), max.y(), max.z()), color); | ||
} | ||
|
||
void DebugLineRender::drawCircle(const Magnum::Vector3& pos, | ||
float radius, | ||
const Magnum::Color4& color, | ||
int numSegments, | ||
const Magnum::Vector3& normal) { | ||
// https://stackoverflow.com/questions/11132681/what-is-a-formula-to-get-a-vector-perpendicular-to-another-vector | ||
auto randomPerpVec = normal.z() < normal.x() | ||
? Mn::Vector3(normal.y(), -normal.x(), 0) | ||
: Mn::Vector3(0, -normal.z(), normal.y()); | ||
|
||
pushTransform(Mn::Matrix4::lookAt(pos, pos + normal, randomPerpVec) * | ||
Mn::Matrix4::scaling(Mn::Vector3(radius, radius, 0.f))); | ||
|
||
Mn::Vector3 prevPt; | ||
for (int seg = 0; seg <= numSegments; seg++) { | ||
Mn::Deg angle = Mn::Deg(360.f * float(seg) / numSegments); | ||
Mn::Vector3 pt(Mn::Math::cos(angle), Mn::Math::sin(angle), 0.f); | ||
if (seg > 0) { | ||
drawTransformedLine(prevPt, pt, color); | ||
} | ||
prevPt = pt; | ||
} | ||
|
||
popTransform(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to reinvent anything :)
I was thinking you could have a generic drawPrimitive()
utility:
void DebugLineRender::drawPrimitive(const Mn::Trade::MeshData& mesh, const Mn::Matrix4& transformation, const Mn::Color4& color) {
/* Turn the mesh into nonindexed lines first */
if(mesh.primitive() != Mn::MeshPrimitive::Lines) {
CORRADE_INTERNAL_ASSERT(mesh.primitive() == Mn::MeshPrimitive::LineLoop ||
mesh.primitive() == Mn::MeshPrimitive::LineStrip);
return drawPrimitive(Mn::MeshTools::generateIndices(mesh), transformation, color);
}
if(mesh.isIndexed())
return drawPrimitive(Mn::MeshTools::duplicate(mesh), transformation, color);
/* Steal all the positions */
for(const Mn::Vector3& position: mesh.attribute<Mn::Vector3>(Mn::Trade::MeshAttribute::Position))
arrayAppend(_verts, Cr::InPlaceInit, transformation.transformPoint(position), color);
}
and then just reuse whatever existing primitive code (didn't you also have a debug renderer with colored axes? there's Primitives::axis3D()
for that too):
void DebugLineRender::drawBox(const Mn::Vector3& min, const Mn::Vector3& max, const Mn::Color4& color) {
drawPrimitive(Mn::Primitives::cubeWireframe(),
_cachedInputTransform*
Mn::Matrix4::translation((max + min)/2.0f)* /* i hope this is correct? */
Mn::Matrix4::scaling((max - min)/2.0f),
color);
}
void DebugLineRender::drawCircle(const Mn::Vector3& pos, float radius, int segments, const Mn::Color4& color, const Mn::Vector3& normal) {
...
drawPrimitive(Mn::Primitives::circle3DWireframe(segments),
_cachedInputTransform*
Mn::Matrix4::lookAt(pos, pos + normal, randomPerpVec)*
Mn::Matrix4::scaling(Mn::Vector3(radius, radius, 0.f)),
color);
}
Add #include
s as needed. The Primitives
library should be sufficiently lightweight to have negligible perf impact when used this way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like your suggestion here is more complicated than what I have. From my perspective, a bit of code to create a circle or a box is simple and readable, and users can easily adapt it later to their own shapes (tetrahedrons, helices, etc.).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code has a lot of extra API calls with long names to massage the mesh data, yes, but the essence of it is a single loop that copies line endpoints from a large gallery of existing primitives, where adding another one (such as a cone for visualizing spotlights) is extremely trivial :)
I commented also because my past experience is that any primitive generation code that wasn't thoroughly tested tended to rot over time, getting increasingly broken during minor refactorings. Since I suppose you won't want to spend time writing tests ensuring the mesh is generated correctly, this would delegate that responsibility to the engine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now, rather than drag out the discussion, let's agree to disagree. :)
I'll add a comment in the file pointing to this discussion, in case folks in the future want to go this route.
src/esp/gfx/DebugLineRender.cpp
Outdated
// 1.2 is hand-tuned for Nvidia hardware to be just small enough so we don't | ||
// see gaps between the individual offset lines. | ||
const float x = _internalLineWidth * 1.2f; | ||
constexpr float sqrtOfTwo = 1.4142f; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/esp/gfx/DebugLineRender.cpp
Outdated
} | ||
|
||
void DebugLineRender::drawPathWithEndpointCircles( | ||
const std::vector<Mn::Vector3>& points, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI you could take an Containers::ArrayView<const Vector3>
, and then if you #include <Corrade/Containers/ArrayViewStl.h>
, the std::vector
is implicitly convertible to it, but it allows you to use any other contiguous container (std::array
, a C array...) as well.
Could be worth it if you're often submitting paths with known sizes and can thus have e.g. Vector3 path[5] { ... }
instead of populating a vector, if it's always a dynamic vector then probably not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I support all efforts to reduce our per-frame dynamic allocations!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmmm, in fact I was calling this in one test with a fixed list, but I hit this:
lineRender->drawPathWithEndpointCircles(
{{0.f, 0.f, 0.f}, {0.1f, 0.f, 0.f}}, radius, color);
viewer.cpp:1156:55: error: cannot convert ‘<brace-enclosed initializer list>’ to ‘Corrade::Containers::ArrayView<const Magnum::Math::Vector3<float> >’
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
C++ is unintuitive. Lengthy answer why is it like that -- see the yellow box titled "Conversion from std::initializer_list" there.
IOW, the API is designed to avoid very common use-after-free issues, which unfortunately makes this one use case annoying. What I commonly do on the engine side is adding a std::initializer_list
overload for every function that takes an ArrayView
. Like
void DebugLineRender::drawPathWithEndpointCircles(const Cr::Containers::ArrayView<const Mn::Vector3> points, ...) { ... }
void DebugLineRender::drawPathWithEndpointCircles(const std::initializer_list<Mn::Vector3> points, ...) {
drawPathWithEndpointCircles(Containers::arrayView(points), ...);
}
Without an overload, in the case above you'd have to do a bit more typing:
lineRender->drawPathWithEndpointCircles(
Containers::arrayView<Vector3>({{0.f, 0.f, 0.f}, {0.1f, 0.f, 0.f}}), radius, color);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok. I'm not too worried about the ugly callsite I gave above; it was just some temp testing code. I don't think the initializer_list
version is necessary.
I've switched to ArrayView and also added a wrapper version that takes a std::vector; this was needed for the Python binding.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. One comment on the bindings.
.def("draw_box", &DebugLineRender::drawBox, | ||
R"(Draw a box in world-space or local-space (see pushTransform).)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No color for box bindings?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
draw_box takes two Vector3 and a color. This binding here just takes all the C++ function parameters as-is without explicitly declaring them all here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this just makes it harder for python only user seeing the doc-string API reference to know the parameters. Also, does implicit conversion allow keyword (named) arguments?
Motivation and Context
Singleton utility class for on-the-fly rendering of lines (e.g. every frame). This is intended for debugging or simple UX for prototype apps. The API prioritizes ease-of-use over maximum runtime performance.
replay_playback3.mp4
How Has This Been Tested
integrated into replay_tutorial.py; also ad-hoc testing in viewer.cpp
Types of changes
Checklist