Files
filament/libs/filagui/src/ImGuiExtensions.cpp
T
Philip Rideout 10251821c6 filagui: Introduce widget for manipulating vectors.
This is our first ImGui extension, it draws a draggable 3D arrow for
manipulating a unit vector. This is especially useful for tweaking the
light direction.
2019-04-04 10:59:38 -07:00

340 lines
13 KiB
C++

/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <filagui/ImGuiExtensions.h>
#include <imgui.h>
#include <math/scalar.h>
#include <math/vec3.h>
#include <math/quat.h>
#include <limits>
using namespace filament::math;
// Private helper class for ImGuiExt::DirectionWidget, heavily inspired by AntTweakBar and Chris
// Maughan's port to ImGui. Thanks Chris!
class ArrowWidget {
public:
ArrowWidget(float3 direction);
bool draw();
float3 getDirection() const;
private:
quatf mDirectionQuat;
enum EArrowParts { ARROW_CONE, ARROW_CONE_CAP, ARROW_CYL, ARROW_CYL_CAP };
static void createArrow();
void drawTriangles(ImDrawList* draw_list, const ImVec2& offset,
const ImVector<ImVec2>& triProj, const ImVector<ImU32>& colLight, int numVertices);
static float quatD(float w, float h) { return std::min(std::abs(w), std::abs(h)) - 4.0f; }
static float quatPX(float x, float w, float h) { return (x*0.5f*quatD(w, h) + w*0.5f + 0.5f); }
static float quatPY(float y, float w, float h) { return (-y*0.5f*quatD(w, h) + h*0.5f - 0.5f); }
static float quatIX(int x, float w, float h) { return (2.0f*x - w - 1.0f) / quatD(w, h); }
static float quatIY(int y, float w, float h) { return (-2.0f*y + h - 1.0f) / quatD(w, h); }
static void quatFromDirection(quatf& quat, const float3& dir);
static ImU32 blendColor(ImU32 c1, ImU32 c2, float t);
const ImU32 DirColor = 0xffff0000;
const int WidgetSize = 100;
};
namespace ImGuiExt {
bool DirectionWidget(const char* label, float v[3]) {
ImGui::PushID(label);
ImGui::BeginGroup();
float3 dir = {v[0], v[1], v[2]};
// DragFloat3 is nicer than SliderFloat3 because it supports double-clicking for keyboard entry.
bool changed = ImGui::DragFloat3(label, &dir.x, 0.01f, -1.0f, 1.0f);
if (changed) {
v[0] = dir.x;
v[1] = dir.y;
v[2] = dir.z;
}
ArrowWidget widget(normalize(dir));
if (widget.draw() && !changed) {
changed = true;
dir = widget.getDirection();
v[0] = dir.x;
v[1] = dir.y;
v[2] = dir.z;
}
ImGui::EndGroup();
ImGui::PopID();
return changed;
}
} // namespace ImGuiExt
static ImVector<float3> s_ArrowTri[4];
static ImVector<ImVec2> s_ArrowTriProj[4];
static ImVector<float3> s_ArrowNorm[4];
static ImVector<ImU32> s_ArrowColLight[4];
inline float ImVec2Cross(const ImVec2& left, const ImVec2& right) {
return (left.x * right.y) - (left.y * right.x);
}
static inline ImVec2 operator+(const ImVec2& lhs, const ImVec2& rhs) {
return ImVec2(lhs.x+rhs.x, lhs.y+rhs.y);
}
static inline ImVec2 operator-(const ImVec2& lhs, const ImVec2& rhs) {
return ImVec2(lhs.x-rhs.x, lhs.y-rhs.y);
}
ArrowWidget::ArrowWidget(float3 direction) {
quatFromDirection(mDirectionQuat, direction);
}
float3 ArrowWidget::getDirection() const {
float3 d = mDirectionQuat * float3(1, 0, 0);
return d / length(d);
}
bool ArrowWidget::draw() {
ImGuiStyle& style = ImGui::GetStyle();
ImDrawList* draw_list = ImGui::GetWindowDrawList();
if (s_ArrowTri[0].empty()) {
ArrowWidget::createArrow();
}
bool value_changed = false;
ImVec2 orient_pos = ImGui::GetCursorScreenPos();
float sv_orient_size = std::min(ImGui::CalcItemWidth(), float(WidgetSize));
float w = sv_orient_size;
float h = sv_orient_size;
// We want to generate quaternion rotations relative to the quaternion in the down press state.
// This gives us cleaner control over rotation (it feels better)
static quatf origQuat;
static float3 coordOld;
bool highlighted = false;
ImGui::InvisibleButton("widget", ImVec2(sv_orient_size, sv_orient_size));
if (ImGui::IsItemActive()) {
highlighted = true;
ImVec2 mouse = ImGui::GetMousePos() - orient_pos;
if (ImGui::IsMouseClicked(0)) {
origQuat = mDirectionQuat;
coordOld = float3(quatIX((int)mouse.x, w, h), quatIY((int)mouse.y, w, h), 1.0f);
} else if (ImGui::IsMouseDragging(0)) {
float3 coord(quatIX((int)mouse.x, w, h), quatIY((int)mouse.y, w, h), 1.0f);
float3 pVec = coord;
float3 oVec = coordOld;
coord.z = 0.0f;
float n0 = length(oVec);
float n1 = length(pVec);
if (n0 > FLT_EPSILON && n1 > FLT_EPSILON) {
float3 v0 = oVec / n0;
float3 v1 = pVec / n1;
float3 axis = cross(v0, v1);
float sa = length(axis);
float ca = dot(v0, v1);
float angle = atan2(sa, ca);
if (coord.x*coord.x + coord.y*coord.y > 1.0) {
angle *= 1.0f + 1.5f*(length(coord) - 1.0f);
}
quatf qrot, qres, qorig;
qrot = quatf::fromAxisAngle(axis, angle);
float nqorig = sqrt(origQuat.x * origQuat.x + origQuat.y * origQuat.y +
origQuat.z * origQuat.z + origQuat.w * origQuat.w);
if (abs(nqorig) > FLT_EPSILON * FLT_EPSILON) {
qorig = origQuat / nqorig;
qres = qrot * qorig;
mDirectionQuat = qres;
} else {
mDirectionQuat = qrot;
}
value_changed = true;
}
}
draw_list->AddRectFilled(orient_pos, orient_pos + ImVec2(sv_orient_size, sv_orient_size),
ImColor(style.Colors[ImGuiCol_FrameBgActive]), style.FrameRounding);
} else {
ImColor color(ImGui::IsItemHovered() ? style.Colors[ImGuiCol_FrameBgHovered] :
style.Colors[ImGuiCol_FrameBg]);
draw_list->AddRectFilled(orient_pos, orient_pos + ImVec2(sv_orient_size, sv_orient_size),
color, style.FrameRounding);
}
ImVec2 inner_pos = orient_pos;
float inner_size = sv_orient_size;
quatf quat = normalize(mDirectionQuat);
ImColor alpha(1.0f, 1.0f, 1.0f, highlighted ? 1.0f : 0.75f);
float3 arrowDir = quat * float3(1, 0, 0);
for (int k = 0; k < 4; ++k) {
int j = (arrowDir.z > 0) ? 3 - k : k;
assert(s_ArrowTriProj[j].size() == (s_ArrowTri[j].size()) &&
s_ArrowColLight[j].size() == s_ArrowTri[j].size() &&
s_ArrowNorm[j].size() == s_ArrowTri[j].size());
size_t ntri = s_ArrowTri[j].size();
for (int i = 0; i < ntri; ++i) {
float3 coord = s_ArrowTri[j][i];
float3 norm = s_ArrowNorm[j][i];
if (coord.x > 0) {
coord.x = 2.5f * coord.x - 2.0f;
} else {
coord.x += 0.2f;
}
coord.y *= 1.5f;
coord.z *= 1.5f;
coord = quat * coord;
norm = quat * norm;
s_ArrowTriProj[j][i] = ImVec2(quatPX(coord.x, w, h), quatPY(coord.y, w, h));
ImU32 col = (DirColor | 0xff000000) & alpha;
s_ArrowColLight[j][i] = blendColor(0xff000000, col, abs(clamp(norm.z, -1.0f, 1.0f)));
}
drawTriangles(draw_list, inner_pos, s_ArrowTriProj[j], s_ArrowColLight[j], ntri);
}
return value_changed;
}
void ArrowWidget::drawTriangles(ImDrawList* draw_list, const ImVec2& offset,
const ImVector<ImVec2>& triProj, const ImVector<ImU32>& colLight,
int numVertices) {
const ImVec2 uv = ImGui::GetFontTexUvWhitePixel();
assert(numVertices % 3 == 0);
draw_list->PrimReserve(numVertices, numVertices);
for (int ii = 0; ii < numVertices / 3; ii++) {
ImVec2 v1 = offset + triProj[ii * 3];
ImVec2 v2 = offset + triProj[ii * 3 + 1];
ImVec2 v3 = offset + triProj[ii * 3 + 2];
// 2D cross product to do culling
ImVec2 d1 = v2 - v1;
ImVec2 d2 = v3 - v1;
float c = ImVec2Cross(d1, d2);
if (c > 0.0f) {
v2 = v1;
v3 = v1;
}
draw_list->PrimWriteIdx(ImDrawIdx(draw_list->_VtxCurrentIdx));
draw_list->PrimWriteIdx(ImDrawIdx(draw_list->_VtxCurrentIdx + 1));
draw_list->PrimWriteIdx(ImDrawIdx(draw_list->_VtxCurrentIdx + 2));
draw_list->PrimWriteVtx(v1, uv, colLight[ii * 3]);
draw_list->PrimWriteVtx(v2, uv, colLight[ii * 3 + 1]);
draw_list->PrimWriteVtx(v3, uv, colLight[ii * 3 + 2]);
}
}
void ArrowWidget::createArrow() {
const int SUBDIV = 15;
const float CYL_RADIUS = 0.08f;
const float CONE_RADIUS = 0.16f;
const float CONE_LENGTH = 0.25f;
const float ARROW_BGN = -1.1f;
const float ARROW_END = 1.15f;
for (int i = 0; i < 4; ++i) {
s_ArrowTri[i].clear();
s_ArrowNorm[i].clear();
}
float x0, x1, y0, y1, z0, z1, a0, a1, nx, nn;
for (int i = 0; i < SUBDIV; ++i) {
a0 = 2.0f*float(M_PI)*(float(i)) / SUBDIV;
a1 = 2.0f*float(M_PI)*(float(i + 1)) / SUBDIV;
x0 = ARROW_BGN;
x1 = ARROW_END - CONE_LENGTH;
y0 = cosf(a0);
z0 = sinf(a0);
y1 = cosf(a1);
z1 = sinf(a1);
s_ArrowTri[ARROW_CYL].push_back(float3(x1, CYL_RADIUS*y0, CYL_RADIUS*z0));
s_ArrowTri[ARROW_CYL].push_back(float3(x0, CYL_RADIUS*y0, CYL_RADIUS*z0));
s_ArrowTri[ARROW_CYL].push_back(float3(x0, CYL_RADIUS*y1, CYL_RADIUS*z1));
s_ArrowTri[ARROW_CYL].push_back(float3(x1, CYL_RADIUS*y0, CYL_RADIUS*z0));
s_ArrowTri[ARROW_CYL].push_back(float3(x0, CYL_RADIUS*y1, CYL_RADIUS*z1));
s_ArrowTri[ARROW_CYL].push_back(float3(x1, CYL_RADIUS*y1, CYL_RADIUS*z1));
s_ArrowNorm[ARROW_CYL].push_back(float3(0, y0, z0));
s_ArrowNorm[ARROW_CYL].push_back(float3(0, y0, z0));
s_ArrowNorm[ARROW_CYL].push_back(float3(0, y1, z1));
s_ArrowNorm[ARROW_CYL].push_back(float3(0, y0, z0));
s_ArrowNorm[ARROW_CYL].push_back(float3(0, y1, z1));
s_ArrowNorm[ARROW_CYL].push_back(float3(0, y1, z1));
s_ArrowTri[ARROW_CYL_CAP].push_back(float3(x0, 0, 0));
s_ArrowTri[ARROW_CYL_CAP].push_back(float3(x0, CYL_RADIUS*y1, CYL_RADIUS*z1));
s_ArrowTri[ARROW_CYL_CAP].push_back(float3(x0, CYL_RADIUS*y0, CYL_RADIUS*z0));
s_ArrowNorm[ARROW_CYL_CAP].push_back(float3(-1, 0, 0));
s_ArrowNorm[ARROW_CYL_CAP].push_back(float3(-1, 0, 0));
s_ArrowNorm[ARROW_CYL_CAP].push_back(float3(-1, 0, 0));
x0 = ARROW_END - CONE_LENGTH;
x1 = ARROW_END;
nx = CONE_RADIUS / (x1 - x0);
nn = 1.0f / sqrtf(nx*nx + 1);
s_ArrowTri[ARROW_CONE].push_back(float3(x1, 0, 0));
s_ArrowTri[ARROW_CONE].push_back(float3(x0, CONE_RADIUS*y0, CONE_RADIUS*z0));
s_ArrowTri[ARROW_CONE].push_back(float3(x0, CONE_RADIUS*y1, CONE_RADIUS*z1));
s_ArrowTri[ARROW_CONE].push_back(float3(x1, 0, 0));
s_ArrowTri[ARROW_CONE].push_back(float3(x0, CONE_RADIUS*y1, CONE_RADIUS*z1));
s_ArrowTri[ARROW_CONE].push_back(float3(x1, 0, 0));
s_ArrowNorm[ARROW_CONE].push_back(float3(nn*nx, nn*y0, nn*z0));
s_ArrowNorm[ARROW_CONE].push_back(float3(nn*nx, nn*y0, nn*z0));
s_ArrowNorm[ARROW_CONE].push_back(float3(nn*nx, nn*y1, nn*z1));
s_ArrowNorm[ARROW_CONE].push_back(float3(nn*nx, nn*y0, nn*z0));
s_ArrowNorm[ARROW_CONE].push_back(float3(nn*nx, nn*y1, nn*z1));
s_ArrowNorm[ARROW_CONE].push_back(float3(nn*nx, nn*y1, nn*z1));
s_ArrowTri[ARROW_CONE_CAP].push_back(float3(x0, 0, 0));
s_ArrowTri[ARROW_CONE_CAP].push_back(float3(x0, CONE_RADIUS*y1, CONE_RADIUS*z1));
s_ArrowTri[ARROW_CONE_CAP].push_back(float3(x0, CONE_RADIUS*y0, CONE_RADIUS*z0));
s_ArrowNorm[ARROW_CONE_CAP].push_back(float3(-1, 0, 0));
s_ArrowNorm[ARROW_CONE_CAP].push_back(float3(-1, 0, 0));
s_ArrowNorm[ARROW_CONE_CAP].push_back(float3(-1, 0, 0));
}
for (int i = 0; i < 4; ++i) {
s_ArrowTriProj[i].clear();
s_ArrowTriProj[i].resize(s_ArrowTri[i].size());
s_ArrowColLight[i].clear();
s_ArrowColLight[i].resize(s_ArrowTri[i].size());
}
}
ImU32 ArrowWidget::blendColor(ImU32 c1, ImU32 c2, float t) {
ImColor color1(c1);
ImColor color2(c2);
float invt = 1.0f - t;
color1 = ImColor((color1.Value.x * invt) + (color2.Value.x * t),
(color1.Value.y * invt) + (color2.Value.y * t),
(color1.Value.z * invt) + (color2.Value.z * t),
(color1.Value.w * invt) + (color2.Value.w * t));
return color1;
}
// Generate a quaternion that rotates (1,0,0) to (dx,dy,dz).
void ArrowWidget::quatFromDirection(quatf& out, const float3& dir) {
float dn = length(dir);
if (dn < FLT_EPSILON * FLT_EPSILON) {
out.x = out.y = out.z = 0;
out.w = 1;
} else {
float3 rotAxis = { 0, -dir.z, dir.y };
if (dot(rotAxis, rotAxis) < FLT_EPSILON * FLT_EPSILON) {
rotAxis.x = rotAxis.y = 0; rotAxis.z = 1;
}
float rotAngle = acos(clamp(dir.x / dn, -1.0f, 1.0f));
out = quatf::fromAxisAngle(rotAxis, rotAngle);
}
}