Hello! Many times in some of my project I needed to display a region of Workspace in a ViewportFrame. However, this is not possible due to the limitations of ViewportFrames. All displayed Parts need to be descendant of ViewportFrame, so in order to display Parts from Workspace you either need to clone everything to your ViewportFrame if you need a static image, or… You must spend countless hours on getting Workspace descendants to replicate inside ViewportFrame only to end up with your script being massive, unreadable and slow.
Because of the problems listed above, I decided to make my own module for binding VPFs to Workspace. There’re somewhat similar modules out there, but they seem to be serving a different purpose, and I couldn’t find any alternatives so I wrote the module myself. It’s pretty simple and lightweight, I tried to optimize it but I think with a little bit of work it can be made even faster, even though I’m satisfied with the current performance. I decided not to upload it on marketplace and instead just make it a signle-file drop-in module:
Source code
--!strict
--!optimize 2
--!native
--[[
VPFBind v1.0.0 by @GulgPlayer
VPFBind allows you to display parts and models from Workspace in ViewportFrames with the maximum performance
Example usage:
local VPFBind = require(script.VPFBind)
local Camera = Instance.new("Camera")
Camera.Parent = script.Parent
script.Parent.CurrentCamera = Camera
VPFBind(script.Parent, { Camera = workspace.CurrentCamera })
]]
local CollectionService = game:GetService("CollectionService")
local HttpService = game:GetService("HttpService")
local Lighting = game:GetService("Lighting")
local RunService = game:GetService("RunService")
local FLIP_CFRAME = CFrame.new(0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 0, 1)
local SB_CFRAME = FLIP_CFRAME * CFrame.fromOrientation(math.pi, -math.pi / 2, 0)
local SBDN_CFRAME = FLIP_CFRAME * CFrame.fromOrientation(math.pi, math.pi / 2, 0)
local SKYBOX: { [string]: Enum.NormalId } = {
SkyboxBk = Enum.NormalId.Right,
SkyboxDn = Enum.NormalId.Bottom,
SkyboxFt = Enum.NormalId.Left,
SkyboxLf = Enum.NormalId.Back,
SkyboxRt = Enum.NormalId.Front,
SkyboxUp = Enum.NormalId.Top,
}
export type VPFBindOptions = {
Camera: Camera?,
RenderDistance: number?,
WorldModel: ("WorldModel" | "Model")?,
OverlapParams: OverlapParams?,
GCInterval: number?,
ReplicateHighPriority: { [string]: {string} }?,
ReplicateLowPriority: { [string]: {string} }?,
ReplicateChildren: {string}?,
CharacterFPS: number?,
HighPriorityFPS: number?,
LowPriorityFPS: number?,
Skybox: boolean?,
SkyboxCelestialBodies: boolean?
}
local function replicateChild(vpfPart: Instance, part: Instance, replicateTypes: {string}, child: Instance, static: boolean?): ()
local shouldReplicate = false
for _, childType in replicateTypes do
if child:IsA(childType) then
shouldReplicate = true
break
end
end
if not shouldReplicate then return end
local vpfChild = Instance.fromExisting(child)
vpfChild.Parent = vpfPart
if not static then
child.Changed:Connect(function(prop)
(vpfChild :: any)[prop] = (child :: any)[prop]
end)
end
part.ChildRemoved:Connect(function(removedChild)
if removedChild == child then
vpfChild:Destroy()
end
end)
end
local ReplicateModelChildren = { "Clothing", "Humanoid" }
local function assign(to: { [string]: any }, from: { [string]: any }?): ()
if from then
for k, v in from do
to[k] = v
end
end
end
-- Unbind VPF from the Workspace and perform clean-up
local function VPFUnbind(): () end
-- Bind VPF to render a chunk of Workspace with specified camera
local function VPFBind(vpf: ViewportFrame, vpfBindOptions: VPFBindOptions?): typeof(VPFUnbind)
local opts: any = {
Camera = nil,
RenderDistance = 1024,
WorldModel = "Model",
OverlapParams = OverlapParams.new(),
GCInterval = 30,
ReplicateHighPriority = { BasePart={ "CFrame" } },
ReplicateLowPriority = { BasePart={ "Transparency", "LocalTransparencyModifier", "Color", "Size" } },
ReplicateChildren = { "Decal", "WrapTarget", "DataModelMesh" },
CharacterFPS = 60,
HighPriorityFPS = 60,
LowPriorityFPS = 24,
Skybox = true,
SkyboxCelestialBodies = Lighting.Sky.CelestialBodiesShown
}
assign(opts, vpfBindOptions)
local id = `VPF-{HttpService:GenerateGUID(false)}`
local gcElapsedTime = 0
local characterSyncElapsedTime = 0
local highPrioritySyncElapsedTime = 0
local lowPrioritySyncElapsedTime = 0
local partsFolder = Instance.new(opts.WorldModel)
partsFolder.Name = "Workspace"
partsFolder.Parent = vpf
local skyboxModel = Instance.new("Model")
skyboxModel.Name = "Skybox"
local vpfSky = Instance.fromExisting(Lighting.Sky)
vpfSky.Parent = vpf
local skybox = Instance.new("Part")
skybox.Name = "Skybox"
skybox.CFrame = SB_CFRAME
skybox.Size = Vector3.one * (opts.RenderDistance * 2)
skybox.Transparency = 1
skybox.Parent = skyboxModel
local skyboxDn = Instance.new("Part")
skyboxDn.Name = "SkyboxDn"
skyboxDn.CFrame = SBDN_CFRAME
skyboxDn.Size = Vector3.one * (opts.RenderDistance * 2)
skyboxDn.Transparency = 1
skyboxDn.Parent = skyboxModel
for side, face in SKYBOX do
local decal = Instance.new("Decal")
decal.Name = side;
decal.Face = face
decal.Texture = Lighting.Sky[side]
decal.Parent = if side == "SkyboxDn" then skyboxDn else skybox
end
skyboxModel.Parent = vpf
local renderSteppedConnection: RBXScriptConnection,
destroyConnection: RBXScriptConnection,
descendantRemovingConnection: RBXScriptConnection
local connections: { [string]: {RBXScriptConnection} } = {}
local function destroyVPFPart(vpfPart: any): ()
for _, connection in connections[vpfPart:GetAttribute("Id")] do
connection:Disconnect()
end
vpfPart.WorkspaceRef.Value:FindFirstChild(id):Destroy()
vpfPart:Destroy()
end
local function createVPFPart(parent: Instance, part: BasePart): BasePart
local vpfPart = Instance.fromExisting(part)
vpfPart:SetAttribute("Id", HttpService:GenerateGUID())
local vpf2workspaceRef = Instance.new("ObjectValue")
vpf2workspaceRef.Name = "WorkspaceRef"
vpf2workspaceRef.Value = part
vpf2workspaceRef.Parent = vpfPart
vpfPart.Parent = parent
local workspace2vpfRef = Instance.new("ObjectValue")
workspace2vpfRef.Archivable = false
workspace2vpfRef.Name = id
workspace2vpfRef.Value = vpfPart
workspace2vpfRef.Parent = part
return vpfPart
end
local function render(deltaTime: number): ()
if not vpf.CurrentCamera then return end
if opts.Camera then
vpf.CurrentCamera.CFrame = opts.Camera.CFrame
end
local camera: Camera = opts.Camera or vpf.CurrentCamera
skybox.Position = camera.CFrame.Position
skyboxDn.Position = camera.CFrame.Position
gcElapsedTime += deltaTime
characterSyncElapsedTime += deltaTime
highPrioritySyncElapsedTime += deltaTime
lowPrioritySyncElapsedTime += deltaTime
local syncCharacters = characterSyncElapsedTime >= 1 / opts.CharacterFPS
local syncHighPriority = highPrioritySyncElapsedTime >= 1 / opts.HighPriorityFPS
local syncLowPriority = lowPrioritySyncElapsedTime >= 1 / opts.LowPriorityFPS
if not syncCharacters and not syncHighPriority and not syncLowPriority then
return
end
if syncCharacters then characterSyncElapsedTime -= 1 / opts.CharacterFPS end
if syncHighPriority then highPrioritySyncElapsedTime -= 1 / opts.HighPriorityFPS end
if syncLowPriority then lowPrioritySyncElapsedTime -= 1 / opts.LowPriorityFPS end
local shouldSync = { syncLowPriority, syncHighPriority, syncCharacters }
local gc = gcElapsedTime >= opts.GCInterval
if gc then gcElapsedTime -= opts.GCInterval end
local parts = workspace:GetPartBoundsInRadius(camera.CFrame.Position, opts.RenderDistance, opts.OverlapParams)
if gc then
for _, vpfPart in partsFolder:GetChildren() do
CollectionService:AddTag(vpfPart, `{id}-GC`)
end
end
for _, part in parts do
if not part:IsA("BasePart") then continue end
local partParent: Instance = partsFolder
local syncPriority = if CollectionService:HasTag(part, "VPF-HighPriority") then 2 else 1
local model = part.Parent
if model:IsA("Accessory") or model:IsA("Tool") then
model = model.Parent
end
if model:FindFirstChildWhichIsA("Humanoid") then
local saved: ObjectValue = model:FindFirstChild(id)
if saved then
partParent = saved.Value :: Instance
else
local vpfModel = createVPFPart(partParent, model)
partParent = vpfModel
for _, child in model:GetChildren() do
replicateChild(vpfModel, model, ReplicateModelChildren, child, true)
end
model.ChildAdded:Connect(function(child)
replicateChild(vpfModel, model, ReplicateModelChildren, child, true)
end)
end
syncPriority = 3
end
if not shouldSync[syncPriority] then continue end
local saved: ObjectValue = part:FindFirstChild(id)
if saved then
local vpfPart = saved.Value :: typeof(part)
for partType, props in opts.ReplicateHighPriority do
if part:IsA(partType) then
for _, prop in props do
vpfPart[prop] = part[prop]
end
end
end
CollectionService:RemoveTag(vpfPart, `{id}-GC`)
else
local vpfPart = createVPFPart(partParent, part)
local partConnections = {}
for partType, props in opts.ReplicateLowPriority do
if part:IsA(partType) then
for _, prop in props do
partConnections[#partConnections + 1] = part:GetPropertyChangedSignal(prop):Connect(function()
(vpfPart :: any)[prop] = part[prop]
end)
end
end
end
connections[vpfPart:GetAttribute("Id")] = partConnections
for _, child in part:GetChildren() do
replicateChild(vpfPart, part, opts.ReplicateChildren, child)
end
part.ChildAdded:Connect(function(child)
replicateChild(vpfPart, part, opts.ReplicateChildren, child)
end)
end
end
if gc then
for _, vpfPart in CollectionService:GetTagged(`{id}-GC`) do
if vpfPart:IsA("Model") then
for _, child in vpfPart:GetChildren() do
if vpfPart:IsA("BasePart") then
destroyVPFPart(child)
end
end
else
destroyVPFPart(vpfPart)
end
end
end
end
renderSteppedConnection = RunService.RenderStepped:Connect(render)
descendantRemovingConnection = workspace.DescendantRemoving:Connect(function(descendant)
local ref = descendant:FindFirstChild(id)
if ref then
destroyVPFPart(ref.Value)
end
end)
local function destroy()
renderSteppedConnection:Disconnect()
destroyConnection:Disconnect()
descendantRemovingConnection:Disconnect()
for _, vpfPart in partsFolder:GetChildren() do
(((vpfPart :: any).WorkspaceRef :: ObjectValue).Value :: BasePart):FindFirstChild(id):Destroy()
end
for _, partConnections in connections do
for _, connection in partConnections do
connection:Disconnect()
end
end
partsFolder:Destroy()
vpfSky:Destroy()
skyboxModel:Destroy()
end
destroyConnection = vpf.Destroying:Connect(destroy)
return destroy
end
return VPFBind
It’s free to use, and you don’t have to give me any credit, the best reward for my work will be just seeing people use it and I hope this module helps you.
Here’s a showcase of what can be achieved using VPFBind:
Demo video (4.3 MB)
Demo place
Code used in demo
local VPFBind = require(script.VPFBind)
local Camera = Instance.new("Camera")
Camera.Parent = script.Parent
script.Parent.CurrentCamera = Camera
VPFBind(script.Parent, { Camera = workspace.CurrentCamera, RenderDistance = 512 })
I actually developed it for my other project, which is a high-quality portal system. While being in proccess of making it, I realized that I don’t actually need this module, but because I have already done a part of work I wonder if anyone else wants me to release this in public. I made a post about this in #development-discussion, but it got removed for being off-topic.
So, what do you think? If you have any ideas or suggestions, feel free to comment! Hope you will find my module useful
- Finish your portal system, I would actually use it in my projects
- Give up on your portal system, it would be less useful than VPFBind
- VPFBind isn’t useful
0 voters