Host NoesisGUI XAML views inside Godot 4 with true WPF-style MVVM in C#. Design UI in Noesis Studio / Blend, bind it to plain INotifyPropertyChanged ViewModels, and composite it like any Godot Control.
Unofficial community integration and is not affiliated with or endorsed by Noesis Technologies (yet?).
There is no official Noesis <-> Godot integration anywhere; this is the first. Verified with Godot 4.7.1 .NET (Forward+/Vulkan) and Noesis 3.2.13: XAML loading, MVVM data binding, commands, styled/templated controls, mouse + keyboard input, and clean shutdown all functional.
- Godot 4+ .NET build (tested on 4.7.1)
- .NET 8 SDK
- A NoesisGUI license - see pricing
- Windows (the current render backend uses WGL; see roadmap)
The addon is pure C# source; the Noesis runtime (managed + native) comes from the public Noesis.GUI NuGet package. No SDK download or Noesis account needed to build.
-
Copy the
addons/noesisgui/folder into your Godot .NET project. Godot compiles it as part of your game assembly automatically. -
Add to your game's
.csproj:<ItemGroup> <PackageReference Include="Noesis.GUI" Version="3.2.*" /> </ItemGroup>
and inside the main
<PropertyGroup>:<!-- Noesis.GUI ships native assets under legacy RIDs (win10-x64); without this .NET 8+ silently skips them (NETSDK1206). --> <UseRidGraph>true</UseRidGraph>
-
Build once, then enable NoesisGodotNet in Project Settings → Plugins.
-
Set your license in Project Settings → NoesisGUI (
noesis_gui/license/name,noesis_gui/license/key), or viaNOESIS_LICENSE_NAME/NOESIS_LICENSE_KEYenvironment variables. Pointnoesis_gui/resources/rootat the folder holding your XAML. -
Add a
NoesisViewnode and set itsXamlproperty. Done.
This repo is itself a Godot project with a working example: open in Godot 4.x .NET (restores NuGet on build), set a license (or run in eval mode), and run examples/HelloNoesis/Main.tscn.
Add a NoesisView node (extends TextureRect), set its Xaml property (relative to noesis_gui/resources/root, or an absolute res:// path), and hand it a ViewModel:
GetNode<NoesisView>("NoesisView").ViewModel = new MainMenuViewModel();The ViewModel is plain .NET. INotifyPropertyChanged, ICommand, zero engine types - so the same UI + logic previews in Noesis Studio and unit-tests outside Godot.
The official Noesis theme loads by default (embedded in the Noesis.App.Theme package), so every standard control - TextBox, Slider, ComboBox, ScrollViewer, ProgressBar has proper styles out of the box. noesis_gui/theme/xaml controls it: the default Theme/NoesisTheme.DarkBlue.xaml, another variant like Theme/NoesisTheme.LightBlue.xaml, a path to your own ResourceDictionary (relative to the resources root or res://), or empty to opt out (then untemplated controls render as Noesis's pink fallback). See examples/ThemeShowcase/.
NoesisView3D puts XAML on a quad in 3D - in-world screens, holo-panels, terminals. Set Xaml, PanelSize (world units), and PixelsPerMeter (texture resolution). Mouse input is raycast from the camera and mapped into the view; the last-clicked panel owns keyboard focus. Hot-reload and ViewModels work exactly like the 2D NoesisView (both wrap the same NoesisViewHost). See examples/WorldSpace/.
Sizing world-space UI. How large a panel appears is entirely a camera/placement decision - the node only controls physical size (PanelSize) and texture density (PixelsPerMeter). Two rules of thumb:
- Apparent size: a panel fills the screen vertically when
PanelSize.y ≈ 2 × distance × tan(fov/2). The example uses a 0.9m-tall panel at 0.9m with a 60° FOV (~87% of screen height). - Crispness vs. cost: pick
PixelsPerMeterso the texture roughly matches the panel's on-screen pixel size at typical viewing distance. Too low looks soft; too high wastes render + readback bandwidth every frame. The XAML's own layout matters too - a fixed-width column designed for fullscreen 2D will occupy only part of a wide panel (wrap content in aViewboxif you want it to scale to fill instead).
- Broken-XAML overlay: if a hot-reload save has invalid markup, the running view keeps the last good frame and shows the parse error in an overlay strip; it clears on the next successful save.
- Cursor forwarding: the OS cursor follows the UI (I-beam over text boxes, hand over hyperlinks) - in 3D, scoped to while the panel is hovered.
- Gamepad: joypad buttons map to Noesis gamepad navigation keys (D-pad focus movement, A/B accept/cancel, shoulder page) on the focused view/panel.
- Project → Tools → Open Selected XAML in Noesis Studio: opens the FileSystem-dock selection in Studio. Set
noesis_gui/editor/studio_pathto the Studio executable, or leave empty to use the OS.xamlassociation.
When running from the editor, the plugin watches noesis_gui/resources/root for .xaml changes. Save a file in Noesis Studio, Rider, anywhere, and the running game updates live: resource dictionaries and templates refresh via Noesis's reload mechanism, and any NoesisView whose root document changed is rebuilt in place with its ViewModel preserved. Invalid markup mid-edit is tolerated (the last good view stays up with a warning). Exported builds skip all of this.
- XAML, fonts (
.ttf/.otf), and XAML-referenced images live undernoesis_gui/resources/root. .xamlfiles are handled by the bundled import plugin: visible in the FileSystem dock and automatically included in exports (asXamlFileresources). No export filters are needed.- XAML-referenced images and fonts still need to ship raw: add them to your export preset's non-resource include filter (e.g.
*.ttf, UI/Images/*), since Godot's importer would otherwise only export the converted.ctexversions. - Fonts: reference by family WPF-style -
FontFamily="./#Orbitron".
Godot _Process → Noesis View.Update → render on a private offscreen GL context (RenderDeviceGL) → readback → ImageTexture shown by the NoesisView control (premultiplied-alpha blend). Input events from _GuiInput are translated 1:1 (mouse, keyboard incl. text input, wheel, touch).
The offscreen-context design works identically under Forward+ (Vulkan) and Compatibility (GL) and can never corrupt Godot's render state. The cost is a GPU→CPU copy per frame which is perfectly fine for menus/HUDs; zero-copy paths are the top roadmap item.
The plugin picks the fastest backend at startup, per view:
- Zero-copy, Compatibility/GL renderer (Windows): a second GL context shared with Godot's renders Noesis directly into a Godot-owned texture via FBO, no per-frame CPU copy. Requires single-threaded Compatibility rendering (the default).
- Zero-copy, Forward+/Mobile (Vulkan) (Windows): a VkImage with exportable memory is allocated on Godot's own Vulkan device, imported into a private GL context via
GL_EXT_memory_object_win32, and handed back to Godot as aTexture2DRD- same GPU memory across both APIs. Currently inactive on stock Godot: the engine doesn't enableVK_KHR_external_memory_win32at device creation. Proposal filed over on godot-proposals; the plugin detects this and uses readback. - Readback (fallback / Linux): private GL/EGL context +
glReadPixels+ texture upload. Works everywhere. Fine for menus/HUDs.
Both zero-copy paths are controlled by noesis_gui/rendering/zero_copy (default on) and fall back to readback automatically with a logged reason.
The startup log prints which backend each view got.
Platforms: Windows (zero-copy under both renderers) and Linux (EGL readback backend, X11, and Wayland - Steam Deck included). macOS is on the roadmap (Godot's GL there runs through ANGLE/Metal, which needs different plumbing).
Theme integration(done in 0.4)Zero-copy Compatibility path(done in 0.7)Vulkan interop(done in 0.9: external-memory zero-copy on Windows; Linux FD-handle variant pending)Editor QoL: XAML preview in the editor(hot-reload + import: 0.2; error overlay, cursor forwarding, gamepad, Studio button: 0.6)World-space UI(done in 0.5:NoesisView3D)Linux render backend(done in 0.8: EGL) / macOS backend (working on getting a test device; ANGLE/Metal path)- C++ GDExtension core for GDScript users (same architecture, native)
This plugin code is yours to license as you wish. NoesisGUI itself is commercial software — your Noesis license governs shipping it. Do not commit license keys; use env vars or an untracked override.cfg.