Parallax Corrected 3D SurfaceGUIs

Parallax Corrected Surface GUIs

(Module)
:astonished: :astonished:


Anyways! I’ve created an open sourced parallax correction module to fake 3D space using UI elements like imagelabels and frames. This can be used for things such as faking the size and scale of things that would normally be in a skybox, but actually have it in 3D space without the need of a gigantic scaled model. The module has support for every UI element that works in a SurfaceGUI, including buttons.

The module WILL NOT work with rotation due to the clip descendants not working with rotation.
The images also cannot be rotated in 3D space as that would require image manipulation which currently is limited to editable image & meshes which aren’t live in-game yet, which is why I haven’t included them in this module.


Showcase Video


Here are some examples :sunglasses:

Examples

TextLabels


Images

Low PixelsPerStud Effect

Moving Text

Videos (works with sound)

BoxInBox effect

Rotating Part


Downloads
play the OLD demo or download the demo file This one doesn’t have any optimisations

Optimised demo and download This one has optimisations and stress testing
ParallaxWindows.rbxm (12.6 KB)


Simple Setup Code

Step 1 - Import the Module
local ParallaxWindows = require(script.ParallaxWindows)

-- initiate a new ParallaxWindows using .new() constructor
local ParallaxWindow = ParallaxWindows.new()
Step 2 - Create your Frame
local part = game.Workspace.Part

local frame = ParallaxWindow:AddFrame(part,Enum.NormalId.Front) -- Create the frame object on a part's face
local originalOffset = Vector3.new(-10,0,0) -- the positional offset of the parallax
ParallaxWindow:UpdateFrameSettings(frame,{ ["PosOffset"] = originalOffset, }) -- update this specific frame's settings, overrides the default ones!
Step 3 - Running the Loop
game:GetService("RunService").RenderStepped:Connect(function()
	-- call Step() for each parallaxwindow object to update all of its frames and guis
	ParallaxWindow:Step()
end)
Final Example Code
local ParallaxWindows = require(script.ParallaxWindows)

-- initiate a new ParallaxWindows using .new() constructor
local ParallaxWindow = ParallaxWindows.new()

local part = game.Workspace.Part

local frame = ParallaxWindow:AddFrame(part,Enum.NormalId.Front) -- Create the frame object on a part's face
local originalOffset = Vector3.new(-10,0,0) -- the positional offset of the parallax
ParallaxWindow:UpdateFrameSettings(frame,{ ["PosOffset"] = originalOffset, }) -- update this specific frame's settings, overrides the default ones!

game:GetService("RunService").RenderStepped:Connect(function()
	-- call Step() for each parallaxwindow object to update all of its frames and guis
	ParallaxWindow:Step()
end)

Change Default Settings
local GuiSettings = {
	["MaxFramerate"] = 60, -- Max Frame rate that the GUI will run at
	["MinFramerate"] = 1, -- Min Frame rate that the GUI will run at (for when the GUI is out of Update Distance)
	["UpdateDistance"] = 200, -- Distance that the GUI will stop updating at (Module will adjust framerate depending on the distance)

	["ZOffset"] = 0,
	["AlwaysOnTop"] = false,
	["Brightness"] = 1,
	["LightInfluence"] = 0,
	["MaxDistance"] = 1000,
	["PixelsPerStud"] = 100,
	["SizingMode"] = Enum.SurfaceGuiSizingMode.PixelsPerStud,
}
local FrameSettings = {
	["UpdateRange"] = 100, -- Distance at which the Image is no longer Updated
	["ZIndex"] = 0,
	["Image"] = "https://assetgame.roblox.com/asset/?id=18271321806&assetName=testface", -- TextureId
	["ImageTransparency"] = 1, -- Transparency of the texture when in range
	["BackgroundTransparency"] = 1, -- Transparency of the texture background when in range
	["ImageColor3"] = Color3.fromRGB(255, 255, 255), -- Color of the texture
	["BackgroundColor3"] = Color3.fromRGB(255, 255, 255), -- Color of the texture background
	["ResampleMode"] = Enum.ResamplerMode.Default, -- Resampling mode
	["ScaleType"] = Enum.ScaleType.Stretch, -- Scaling type
	["ImageSize"] = Vector3.new(8, 8, 0), -- Size of the image in studs per tile, dont use Z value, just using V3 cause its more efficient than V2
	["PosOffset"] = Vector3.new(0, 0, 0), -- Positional offset for parallax from the face
}

-- update default frame settings with initial customization settings
ParallaxWindow:UpdateSettings(GuiSettings,FrameSettings)
Customizable Settings
-- default settings, can be overriden with custom settings by using UpdateFaceSettings() and UpdateFrameSettings()
local DEFAULT_GUI_SETTINGS = {
	["MaxFramerate"] = 60, -- Max Frame rate that the GUI will run at
	["MinFramerate"] = 1, -- Min Frame rate that the GUI will run at (for when the GUI is out of Update Distance)
	["UpdateDistance"] = 200, -- Distance that the GUI will stop updating at (Module will adjust framerate depending on the distance)
	-- SurfaceGui settings
	["ZOffset"] = 0,
	["AlwaysOnTop"] = false,
	["Brightness"] = 1,
	["LightInfluence"] = 0,
	["MaxDistance"] = 1000,
	["PixelsPerStud"] = 100,
	["SizingMode"] = Enum.SurfaceGuiSizingMode.PixelsPerStud,
}

local DEFAULT_FRAME_SETTINGS = {
	["UpdateRange"] = 100, -- Distance at which the Image is no longer Updated
	["ZIndex"] = 0, -- ZIndex of the frame
	["Image"] = "rbxassetid://18838056070", -- ImageId
	["ImageTransparency"] = 0, -- Transparency of the image when in range
	["BackgroundTransparency"] = 1, -- Transparency of the image background when in range
	["ImageColor3"] = Color3.fromRGB(255, 255, 255), -- Color of the image
	["BackgroundColor3"] = Color3.fromRGB(255, 255, 255), -- Color of the image background
	["ResampleMode"] = Enum.ResamplerMode.Default, -- Resampling mode
	["ScaleType"] = Enum.ScaleType.Stretch, -- Scaling type
	["ImageSize"] = Vector3.new(8, 8, 0), -- Size of the image in studs per tile, dont use Z value, just using V3 cause its more efficient than V2
	["PosOffset"] = Vector3.new(0, 0, 0), -- Positional offset for parallax from the face
}

And that’s all! You have my full permission to use this module in your games (credit in the description would be nice tho). I hope you can get some use out of this module. If you have any questions or feedback just send a response to this post!

FYI this is also my first module I’ve created so :+1:

78 Likes

Uh… how the heck did you make this?! AMAZING! Very cool, it’s like those portal games. Very awesome!

2 Likes

really cool module, i can see a lot of potential use cases!
one thing i would recommend is to switch RunService.RenderStepped into RunService.Heartbeat for less impact on performance.
another thing you can do is to turn the :Step() function into a :Start() and :Stop() so its simpler to use!
other than that it looks really nice :)

1 Like

Yeah I would like to use heartbeat, but since it works with the camera, using heartbeat instead of renderstepped makes the images lag behind a bit, which isn’t that noticeable if the images are far away or secluded but if you’re right in front of it, you’ll notice it! I might add a note for others.

And as for the starting and stopping, I kinda wanted to give developers as much control over it as possible. Limiting it to start and stop would make it so they wouldn’t be able to update the module at whatever frame rate they wanted. So whilst it is a bit more confusing I’m sure it’ll be fine. Hopefully

Thanks for the feedback though!

2 Likes

Why don’t you try RunService.PreRender?

This is a really nice module!

One thing I noticed with the block example was that when its rotated, some of the GUIs don’t have a proper parallax effect. When the rotation is (0, 0, 0) however the GUIs work well:

Could this be fixed by doing the parallax calculations in local space of the object so that the object’s rotation doesn’t affect the outcome?

2026-01-23T21:00:00Z

I think the rotation that’s being referred to in the quote is the Rotation property of GuiObjects.

The latter point I think refers to how the image itself can’t be rotated in 3D space since you can’t transform the image’s corners (for example to skew the image):

The problem with rotating the block with 3D SurfaceGUIs comes down to how the parallax math is done for the GUIs.

3 Likes

Yes you are correct. The module doesn’t support the rotation of UIElements, and since we currently don’t have any way of easily morphing images or shaders support we have to settle with stuff like that. I have made support for rotated planes here, but since the editable mesh warps the texture the effect is ruined. And also it’s not that performant or available ingame which is why I left it out for this module.

And as for the rotated parts issue, I’ll probably look into fixing it, but I’m knee deep in my finals exams right now so it probably won’t be out for a month or two. :skull:

2 Likes

That parallax mapping with Editable Meshes looks really convincing :open_mouth:
The issue I mentioned is kind of a nitpick, otherwise the module works great!
It only ever causes issues when the 3D SurfaceGuis are applied to a cube like in the example. The Highlight makes the issue more apparent but with no highlight the GUIs look fine. Also good luck on the final exams!

1 Like

I was just messing around with the effect but I found that if you stacked a lot of the frames really close together you could get the illusion of a 3D object


I also discovered that there were a couple issues with the module so I’m going to fix them soon

3 Likes

Hi, can you add these to the editable demo place ?

2 Likes

I moved the code to this place here, and it is also uncopylocked so you can download it. And this one works in game this time.

2 Likes

Hello again,

I’ve gone through the code and made a bunch of optimsations to the module, allowing more surfaces to be rendered, dynamic frame rates which adjust depending on how far the surface is away from the camera, max and min frame rates so that the user has more control over which surfaces to prioritise.

As for how much it improved the module, Here’s an example of 6561 of them running on an iPhone 13:





The footage above had the updates capped to 200 surfaces per frame, this one below is capped at 1000 updates per frame:

4 Likes

As someone who needs exactly 6,561 Parallax-corrected UIs in my game, I have to say…impressive.

8 Likes

Alright, Finally I think I’ve got a good version of this module.

I added a load more optimisation controls and automatic culling (since before it was broken). This example below uses 2-3 Frames for each window and bunches 9 windows into one surface to cut down on the amount of calculations required. :slightly_smiling_face: It took a lot of math

The place file is here

8 Likes

This is amazing, could you create one that allows meshparts on a frame?
If you made that a module, you could single handedly make parrallex maped mesh enviorments, functionality such as bulletholes, or advanced interiors, etc

I would if I could, but since the module uses Images and UI elements to render stuff I don’t think it would be possible to do it with meshes. I think what you’re looking for is viewportframes since they allow you to render a completely separate workspace, but since they’re kinda laggy I decided to make it using images instead. I am experimenting with editablemesh though and I’ll try to do what I can to make them work with the module. But for now we’re gonna have to settle with flat images.

Also if roblox added shaders this would be way easier :sob:

2 Likes

Do you mind re-uploading a new place file? The current ‘optimised’ one seems to require the person to keep re-downloading studio. I can’t edit it.

If you’re referring to this place, I downloaded its .rbxl file:
testing.rbxl (119.8 KB)