cz imgui / vr gorilla tag menu
about
cz imgui is a dear imgui based menu for gorilla tag. the menu lives on the player's left wrist as a textured quad. the right hand drives a cursor that is projected onto the panel; the right trigger acts as click. text is rasterized on the cpu through a software path and uploaded to a unity texture2d every frame.
two button binds: hold left b or left grip to show the menu while held.
build & inject
./gradlew :app:assembleRelease
output:
app/build/intermediates/stripped_native_libs/release/
stripReleaseDebugSymbols/out/lib/arm64-v8a/libcz_imgui.so
drop into lib/arm64-v8a/ inside the gorilla tag apk, then add the following smali after onCreate (locals 2):
const-string v1, "cz_imgui"
invoke-static {v1}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
repack, sign, sideload.
file layout
app/src/main/cpp/
src/
native-lib.cpp // hooks, mods, ui
czrender.cpp // software triangle raster
include/
czrender.hpp
XRInput.hpp / XRInput.cpp
BNMResolve.hpp // unity api wrappers
extern/
BNM-Android/
Dobby/
imgui/ // dear imgui v1.91.5
register a mod
two functions register a mod into the global registry. both take a category string, a mod name, and a lambda body. categories become collapsing headers in the menu; names become checkboxes inside.
addtick(const char* category, const char* name, void(*fn)())the lambda runs every frame while the checkbox is on. use this for anything continuous: movement, force, polling input, applying a field repeatedly.
addoneshot(const char* category, const char* name, void(*fn)())the lambda runs once on the rising edge (the frame the checkbox flips from off to on). use this for one-time setters like gravity, reset values, applying a preset.
where to put them
everything goes inside registermods() in native-lib.cpp. it is called once on first tick after hands are found.
static void registermods() {
if (!g_mods.empty()) return;
addtick("movement", "long arms", []() {
gtag::setpf("maxArmLength", 999.0f);
});
addoneshot("gravity", "zero", []() {
Physics::SetGravity(Vector3(0, 0, 0));
});
}
tick vs oneshot
| type | fires | typical use |
|---|---|---|
| tick | every frame while on | fly, speed force, holding a key, polling trigger |
| oneshot | once when toggled on | set gravity, set jump multiplier, scale arms |
oneshot does not undo itself when toggled off. add a paired oneshot ("fix gravity", "normal gravity", etc.) to restore.
gtag helpers
shortcuts for accessing the local gorillalocomotion.player singleton and common transforms. all helpers handle null silently.
returns the player instance pointer, or nullptr if not in the scene yet.
read/write a float field by name. examples: "jumpMultiplier", "maxJumpSpeed", "maxArmLength", "velocityLimit", "defaultSlideFactor".
set a bool field. e.g. "disableMovement", "wasLeftHandTouching".
set a vector3 field. e.g. "rightHandOffset", "leftHandOffset".
head returns the head collider transform; body returns the body collider transform; rb returns playerRigidBody; roottrans finds the "GorillaPlayer" root transform.
xr input
three static methods. Controller::Left and Controller::Right select the hand.
features: PrimaryButton, SecondaryButton, GripButton, TriggerButton, MenuButton, Primary2DAxisClick, Primary2DAxisTouch, Secondary2DAxisClick, Secondary2DAxisTouch, PrimaryTouch, SecondaryTouch.
features: Trigger, Grip. value in [0,1].
features: Primary2DAxis, Secondary2DAxis. thumbstick coords.
examples
fly forward while holding right trigger
addtick("movement", "fly", []() {
float t = XRInput::GetFloatFeature(Trigger, Right);
if (t < 0.5f) return;
Transform* h = gtag::head();
Transform* r = gtag::roottrans();
Rigidbody* body = gtag::rb();
if (!h || !r) return;
Vector3 step = h->GetForward() * Time::GetDeltaTime() * 12.0f;
r->SetPosition(r->GetPosition() + step);
if (body) body->SetVelocity(Vector3(0,0,0));
});
bounce on left trigger
addtick("movement", "bounce", []() {
if (!XRInput::GetBoolFeature(TriggerButton, Left)) return;
auto* body = gtag::rb();
if (body) body->AddForce(Vector3(0, 3.5f, 0), ForceMode::VelocityChange);
});
preset speed boost (oneshot)
addoneshot("movement", "speed boost", []() {
gtag::setpf("jumpMultiplier", 2.25f);
gtag::setpf("maxJumpSpeed", 999.0f);
});
headless
addtick("body", "headless", []() {
Transform* h = gtag::head();
if (h) h->SetLocalScale(Vector3(0,0,0));
});
widgets
dear imgui's widget set works inside buildui(). anything you put there is rendered to the panel.
button
if (ImGui::Button("do thing", ImVec2(-1, 28))) {
// fires once on click
}
-1 width = stretch to full panel width.
checkbox
static bool on = false;
ImGui::Checkbox("enable", &on);
note: the mod registry already builds checkboxes for every registered mod, so prefer addtick / addoneshot for actual mod state.
slider
static float v = 1.0f;
ImGui::SliderFloat("strength", &v, 0.1f, 5.0f, "%.2f");
collapsing header
if (ImGui::CollapsingHeader("category", ImGuiTreeNodeFlags_DefaultOpen)) {
// widgets inside
}
separator and spacing
ImGui::Separator();
ImGui::Spacing();
color picker
static ImVec4 col(1,0,0,1);
ImGui::ColorEdit4("tint", (float*)&col);
text
plain text
ImGui::Text("%.0f fps", io.Framerate);
ImGui::TextUnformatted("static line");
ImGui::TextDisabled("%d players", n);
ImGui::TextColored(ImVec4(1,0.4f,0.4f,1), "danger");
gradient text
czstyle::gradienttext(const char* text, float r1, float g1, float b1, float r2, float g2, float b2, float a = 1.0f)renders each character with a color lerped from (r1,g1,b1) to (r2,g2,b2) across the string. shared alpha defaults to 1.
// pink -> cyan
czstyle::gradienttext("cz imgui", 1.0f, 0.4f, 0.9f, 0.3f, 0.7f, 1.0f);
// amber -> deep red with 80% alpha
czstyle::gradienttext("warning", 1.0f, 0.7f, 0.2f, 0.7f, 0.1f, 0.1f, 0.8f);
gradienttext with the same string twice in one window per frame. characters are placed via SameLine(0,0) and identified by string hash; collisions can cause focus state issues.
colors
every helper takes four floats: (r, g, b, a), each in [0, 1]. alpha defaults to 1.0f when omitted.
| helper | affects |
|---|---|
czstyle::background(r,g,b,a) | panel fill |
czstyle::bordercolor(r,g,b,a) | outline & separators |
czstyle::titlecolor(r,g,b,a) | title bar |
czstyle::accent(r,g,b,a) | hover/active highlights |
czstyle::buttoncolor(r,g,b,a) | button default fill |
czstyle::framecolor(r,g,b,a) | slider/checkbox background |
czstyle::headercolor(r,g,b,a) | collapsing header default |
czstyle::textcolor(r,g,b,a) | body text |
czstyle::checkmark(r,g,b,a) | check tick color |
defaults in initimgui:
czstyle::background (0.06f, 0.07f, 0.10f, 0.78f);
czstyle::bordercolor (0.35f, 0.55f, 0.95f, 0.55f);
czstyle::titlecolor (0.15f, 0.40f, 0.85f, 0.95f);
czstyle::headercolor (0.18f, 0.30f, 0.55f, 0.55f);
czstyle::buttoncolor (0.20f, 0.24f, 0.32f, 0.85f);
czstyle::framecolor (0.12f, 0.14f, 0.20f, 0.85f);
czstyle::accent (0.30f, 0.55f, 0.95f, 0.95f);
czstyle::checkmark (0.30f, 0.95f, 0.50f, 1.00f);
czstyle::textcolor (0.95f, 0.96f, 1.00f, 1.00f);
sizes
czstyle::rounding(float px)sets window, child, frame, popup, scrollbar, grab rounding from one value (proportional). 0 = sharp, 14 = default, 24+ = chunky.
czstyle::bordersize(float px)outline thickness. 0 disables.
czstyle::padding(float x, float y)inner padding inside windows. frame padding scales from this (0.85x, 0.6y).
czstyle::spacing(float x, float y)spacing between widgets.
czstyle::fontscale(float scale)multiplies all text size. 1.0 = native bitmap font (small), 1.4 = current default, 2.0 = large.
panel size in world
the physical panel is set by constants at the top of native-lib.cpp:
static constexpr float kpanelmetersw = 0.18f; // width in meters
static constexpr float kpanelmetersh = 0.24f; // height in meters
change and rebuild. the framebuffer resolution stays at 384x512 regardless.
panel offset from the left wrist and rotation are inside czstate:
Vector3 localoffset = {0.0f, 0.10f, 0.04f};
Quaternion localrotoffset = Quaternion::FromEuler(65.0f, 0.0f, 0.0f);
FromEuler is bnm's order: (yaw, pitch, roll). positive yaw rotates around y; positive pitch is around x.
render pipeline
- dear imgui builds widgets on the cpu inside
buildui(). ImGui::Renderproduces aImDrawDatawith triangle lists.cz::rasterizedrawdatawalks each triangle, samples the font atlas, alpha-blends into a 384x512 rgba32 cpu framebuffer.- rows are uploaded reversed (cpu y-flip) into a fresh managed
byte[]per frame. LoadRawTextureData(byte[])+Apply(false,false)commits to the gpu texture.- the texture is sampled by a quad parented to the left hand controller; shader picked from a priority list, defaults to
"Universal Render Pipeline/Unlit".
cpu cost per frame: clear framebuffer (~700kb memset) + rasterize triangles + memcpy + apply. easily fits inside a quest 2 frame budget at 384x512.
hooks & loading
entry: JNI_OnLoad registers onloaded with bnm and calls BNM::Loading::TryLoadByJNI. once il2cpp resolves, onloaded runs.
static void onloaded() {
gcz.playercls = BNM::Class("GorillaLocomotion", "Player");
auto cc = BNM::Class("GorillaNetworking", "CosmeticsController");
auto update = cc.GetMethod("Update", 0);
BNM::InvokeHook(update, hook_ccupdate, orig_ccupdate);
}
the hook on CosmeticsController::Update is chosen because the class is loaded from the very first scene; GorillaLocomotion.Player.Update only fires after a local player spawns.
every frame, tick():
- finds
LeftHand ControllerandRightHand ControllerviaGameObject::Find - iterates
g_modsand calls each enabled mod's lambda - polls the show/hide button (left b or left grip)
- updates the panel transform from
localoffset+localrotoffset - projects the right hand onto the panel plane to feed imgui mouse
- renders the imgui frame, rasterizes, uploads