[FREE RELEASE] cr-3dnui - Interactive 3D DUI panels for FiveM —( real web apps rendered on in-game surfaces)

:sunglasses:

this is just to share my progress in using this. this is in fact the coolest script ive played with

3 Likes

That looks really good, the camera detach is excellent!

This is literally what I envisioned when I thought of this… walk up to ATM and actually press the buttons kind of thing

It’s also makes me very happy that you’re making this and using my library!

But more importantly you’re helping me shape it by giving me these little edge cases so I could put up guard rails making it much easier for devs to strap in.

Tonight when I’m done with my day job I will crack at the new mouse input interactions.

2 Likes

cr-3dnui — Update v2.4

I’ve been iterating hard based on real-world use cases + edge cases.

What’s new

Screen alignment helpers (tilted monitor props)

Many GTA/FiveM monitor models are not perfectly flat (slightly tilted back), which can cause bezel clipping even when your normal looks “correct”.

New options to make panels behave more like a real screen:

  • depthCompensation = "screen" — safer depth bias for tilted props / bezel clipping
  • frontOnly = true — only render + accept input when viewing the front (prevents backside interaction)

“Static vs Live” attachments (bones/entities)

If a panel looks “stuck” when attached to an entity/bone, it’s usually not updating transforms.

To make it behave like a real attachment (like the vehicle demo):

  • updateInterval = 0 — per-frame transform updates
  • updateMaxDistance = 120 — optional distance culling

Whiteboard demo: dual interaction modes

The whiteboard demo now supports both:

  • UV/Raycast (world-space UV mapping)
  • KEY2DUI (cursor rendered inside the DUI + forwarded input)

KEY2DUI is especially useful for constrained camera scenarios (vehicles / tight desks) because it avoids the “UV fighting the camera” feeling.

Internal refactor (no API breakage)

Core client code was split into smaller modules for maintainability. Exports remain backwards compatible.

1 Like

been loving following the quick progress on this. keep up the good work!

1 Like

→ Update v2.5

panel → prop → bone :x:
prop → bone + panel → bone :white_check_mark:

-- Keep the PROP as the bone-attached visual object (true entity parenting),
-- but keep the PANEL attached to the VEHICLE using a baked offset/normal derived from the prop.
-- This removes jitter while still looking like the panel is mounted to the prop.

-- Helper: normalize a vector3
local function vNorm(v)
  local l = #(vector3(v.x, v.y, v.z))
  if l < 0.000001 then return vector3(0.0, 0.0, 1.0) end
  return vector3(v.x / l, v.y / l, v.z / l)
end

-- 1) Spawn prop and attach it to a vehicle bone (inherits full bone animation)
local function attachPropToBone(veh, boneName, model, off, rot)
  local bone = GetEntityBoneIndexByName(veh, boneName)
  if bone == -1 then return nil end

  RequestModel(model)
  while not HasModelLoaded(model) do Wait(0) end

  local prop = CreateObject(model, 0.0, 0.0, 0.0, false, false, false)

  -- make it non-physical (no collision, no damage)
  SetEntityCollision(prop, false, false)
  SetEntityCompletelyDisableCollision(prop, true, true)
  SetEntityHasGravity(prop, false)

  AttachEntityToEntity(
    prop, veh, bone,
    off.x, off.y, off.z,
    rot.x, rot.y, rot.z,
    false, false, false, true, 2, true
  )

  SetModelAsNoLongerNeeded(model)
  return prop
end

-- 2) Bake the panel transform from the prop into VEHICLE LOCAL space
local function bakePanelFromProp(veh, prop, panelLocalOffsetOnProp, panelLocalNormalOnProp)
  -- where the panel should be in WORLD space (prop-local offset)
  local worldPos = GetOffsetFromEntityInWorldCoords(
    prop,
    panelLocalOffsetOnProp.x,
    panelLocalOffsetOnProp.y,
    panelLocalOffsetOnProp.z
  )

  -- convert that WORLD position into VEHICLE LOCAL offset (for stable attach)
  local bakedOffset = GetOffsetFromEntityGivenWorldCoords(veh, worldPos.x, worldPos.y, worldPos.z)

  -- compute WORLD normal direction from prop-local "normal"
  local propOrigin = GetEntityCoords(prop)
  local worldDirPoint = GetOffsetFromEntityInWorldCoords(
    prop,
    panelLocalNormalOnProp.x,
    panelLocalNormalOnProp.y,
    panelLocalNormalOnProp.z
  )
  local worldDir = vNorm(vector3(worldDirPoint.x - propOrigin.x, worldDirPoint.y - propOrigin.y, worldDirPoint.z - propOrigin.z))

  -- convert that WORLD direction into VEHICLE LOCAL direction (baked normal)
  local vehOrigin = GetEntityCoords(veh)
  local vehLocalDirPoint = GetOffsetFromEntityGivenWorldCoords(
    veh,
    vehOrigin.x + worldDir.x,
    vehOrigin.y + worldDir.y,
    vehOrigin.z + worldDir.z
  )
  local bakedNormal = vNorm(vehLocalDirPoint)

  return bakedOffset, bakedNormal
end

-- 3) Stable setup: prop -> bone, panel -> vehicle (baked from prop)
local function mountDashPanelStable(veh)
  local prop = attachPropToBone(veh, DASH_BONE, DASH_PROP_MODEL, DASH_PROP_OFFSET, DASH_PROP_ROT)
  if not prop then return nil, nil end

  local bakedOffset, bakedNormal = bakePanelFromProp(veh, prop, MONITOR_LOCAL_OFFSET, MONITOR_LOCAL_NORMAL)

  local panelId = exports["cr-3dnui"]:AttachPanelToEntity({
    entity      = veh,
    localOffset = bakedOffset,   --  stable position
    localNormal = bakedNormal,   --  correct facing (vehicle-local)
    width       = MONITOR_W,
    height      = MONITOR_H,
    rotateNormal = true,         --  keep it vehicle-relative
  })

  return panelId, prop
end


attaching a panel to a prop then attaching that prop to a bone creates limitations with updating the position of the panel fast enough with the position of the car

its basically extra math for no reason since your no calculating car/prop/panel
meaning more steps

but if we instead use the prop to calulate the offset then attach panel to bone from that offset we could fake it being attached to the prop but have no jitter when moving fast or driving

the only limitation this really presents is that each vehicle will have unique offsets for the position of the panel

so ive created a simple tool to move the prop XYZ and ROT

and you simply use your mouse wheel to modify values

this way (if needed) you can modify the positions and use a prop based panel in any car – *instead of 8 hours of hair pulling–

How to use the car debug (steps)

  • 1 /nuidash
    this is the command to attach the panel and the prop to your car.
    (dont worry if they arent lined up even with your offset)

  • 2 /nuidashprop
    to bind the panel to prop ( this is REQUIRED to be used BEFORE /dashprint)

  • 3 /dashtune
    this will enter the mode that allows you to modify the X/Y/Z/rX/rY/rZ
    simply use your scroll wheel to adjust the location + or -
    for small adjustments hold CTRL
    for big adjustments hold SHIFT

  • 4/dashaxis <x|y|z|rx|ry|rz>
    use commands like /dashaxis x or /dashaxis rz to modify individual parameters
    rx/ry/rz are rotational variables

  • 5/dashprint
    once your happy with the positions this command will print to the F8 console for easy copy pasta

MUST USE /nuidashprop BEFORE /dashprint to have the COMPLETE output like image below:

Basically NOW in 5 easy steps you can attach a prop with a panel to any car!


Note:
(The debug tools default settings use the Elegy2 as the car and prop_monitor_01a as the prop but you can change them but each car and prop must be tuned individually)

Panels and ROT (WORK IN PROGRESS)

3D-NUI panels are not real entities, so they do not automatically inherit full parent rotation; their orientation must be explicitly recomputed from the parent’s live transform if you want true roll/pitch/yaw behavior.

solution: “reverse gravity” lol - seriously

The solution is to stop baking the panel’s normal – and instead recompute it continuously from the vehicle’s live right/forward/up vectors, using the panel’s vehicle-space rest orientation as input.

Almost acting like gravity or magnets where it pulls / pushes where it needs to in order to match the car.

-- panel's REST orientation in VEHICLE SPACE (never changes)
-- this is what you tuned
local PANEL_REST_NORMAL = vector3(nx, ny, nz)

-- every update (frame or throttled loop)
local function updatePanelNormal(panelId, vehicle)
    -- get the vehicle’s live axes (already include roll/pitch/yaw)
    local right, forward, up, _ = GetEntityMatrix(vehicle)

    -- rebuild the panel normal from the vehicle’s current orientation
    local dynamicNormal =
        right   * PANEL_REST_NORMAL.x +
        forward * PANEL_REST_NORMAL.y +
        up      * PANEL_REST_NORMAL.z

    -- normalize (important)
    dynamicNormal = dynamicNormal / #(dynamicNormal)

    -- apply to the panel
    exports['cr-3dnui']:SetPanelNormal(panelId, dynamicNormal)
end

In theory this should work-- or something similar.


TL:DR
i did the annoying part and made a debug tool to make it easy to get the locations/offsets/normals/rots… i also solved the prop > bone + panel > bone nightmare solving the jittery panels issue on moving cars and i came up with a plan to tackle panels to follow cars rotations.
The library is updated to 2.5 (only the car demo has changed)

edit: currently debugging and fixing the panel ROT to allgin with car

1 Like

v2.5 (bug fix)

How ROT (Roll / Pitch / Yaw) Is Actually Solved

3D-NUI panels are not entities.

That means:

  • They will never inherit rotation
  • Roll will not exist unless you explicitly provide it
  • A normal alone is not enough

ROT requires two vectors:

  • localNormal → facing
  • localUp → roll reference

Library requirement (what changed in the lib)

The library must support an explicit up vector:

panelUp

Internally, the panel basis is built from:

normal
panelUp
right = cross(panelUp, normal)
up    = cross(normal, right)

Without panelUp, true roll is impossible.
This was the library-side limitation that had to be fixed.


Where the rotation actually comes from

ROT comes from rebuilding the panel orientation from a live transform.

That source is the prop attached to the bone.

You do not attach the panel to the prop.

Instead:

prop → bone           (real rotation)
panel → vehicle       (stable)

Then you bake the prop’s orientation into vehicle-local space.


Baking ROT correctly (core snippet)

From demo (simplified):

-- prop local → world
local worldNormal = pr * propNormal.x + pf * propNormal.y + pu * propNormal.z
local worldUp     = pr * propUp.x     + pf * propUp.y     + pu * propUp.z

-- world → vehicle local
local localNormal = normalize(worldNormal • vehicleAxes)
local localUp     = normalize(worldUp     • vehicleAxes)

This gives you:

  • a vehicle-local normal
  • a vehicle-local up
  • full roll / pitch / yaw preserved

That’s ROT.

The demo-side fix (this is where it broke)

In the demo, Originally was:

localNormal = vNeg(bakedNormal)
localUp     = vNeg(bakedUp)

That mirrors the basis.

Result:

  • upside-down UI
  • backwards text
  • 180° in-plane rotation

The actual fix (one line)

localNormal = vNeg(bakedNormal)
localUp     = bakedUp

That’s it.

  • Flip normal → controls facing
  • Keep up → preserves roll + UI orientation

Final Result

After this:

  • Panel follows yaw
  • Panel follows pitch
  • Panel follows roll
  • No jitter
  • No camera hacks
  • No per-frame math spam
  • UI orientation stays correct

TL:DR

ROT is solved by:

  1. Library supports panelUp
  2. ROT is baked from a real entity (prop → bone)
  3. Panel attaches to vehicle using baked normal + up
  4. Only the normal is negated — never the up

(If any one of those is missing, ROT breaks)




—NEXT UPDATE: v2.6:

WORK IN PROGESS

New panel creation method
(MUCH EASIER)

Replace texture


Instead of calculating all the fancy math to place a panel on a prop…

This replaces the texture inside the prop with our 3d-NUI
Simply open code walker and search the model and look up the textures in that model
over ride the texture!

still working on focus mode and a few things but should be pushed tonight


Laptop with 3dnui injected via texture override

Animated 3dnui injected via texture override into Yankton plate
(that means WORKING registration tags for cars and animated plates)