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

typefirestypical use
tickevery frame while onfly, speed force, holding a key, polling trigger
oneshotonce when toggled onset 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.

void* gtag::player()

returns the player instance pointer, or nullptr if not in the scene yet.

void gtag::setpf(const char* field, float value) float gtag::getpf(const char* field)

read/write a float field by name. examples: "jumpMultiplier", "maxJumpSpeed", "maxArmLength", "velocityLimit", "defaultSlideFactor".

void gtag::setpb(const char* field, bool value)

set a bool field. e.g. "disableMovement", "wasLeftHandTouching".

void gtag::setpv3(const char* field, Vector3 value)

set a vector3 field. e.g. "rightHandOffset", "leftHandOffset".

Transform* gtag::head() Transform* gtag::body() Rigidbody* gtag::rb() Transform* gtag::roottrans()

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.

bool XRInput::GetBoolFeature(BoolFeature feat, Controller con)

features: PrimaryButton, SecondaryButton, GripButton, TriggerButton, MenuButton, Primary2DAxisClick, Primary2DAxisTouch, Secondary2DAxisClick, Secondary2DAxisTouch, PrimaryTouch, SecondaryTouch.

float XRInput::GetFloatFeature(FloatFeature feat, Controller con)

features: Trigger, Grip. value in [0,1].

Vector2 XRInput::GetVector2Feature(Vector2Feature feat, Controller con)

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);
do not call 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.

helperaffects
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

  1. dear imgui builds widgets on the cpu inside buildui().
  2. ImGui::Render produces a ImDrawData with triangle lists.
  3. cz::rasterizedrawdata walks each triangle, samples the font atlas, alpha-blends into a 384x512 rgba32 cpu framebuffer.
  4. rows are uploaded reversed (cpu y-flip) into a fresh managed byte[] per frame.
  5. LoadRawTextureData(byte[]) + Apply(false,false) commits to the gpu texture.
  6. 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 Controller and RightHand Controller via GameObject::Find
  • iterates g_mods and 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