I have been working on a tornado system which stems from an open source one (Make a billboard gui Tornado) that was originally intended for tornadoes that uses billboard guis, however I am modifying the code to make my own full fledged system which uses ImageHandleAdornments instead of BillboardGuis due to visual issues (they look weird at certain angles especially when they constantly face the camera and you look up and down).
Tried to replicate the facing camera behavior without it looking weird by modifying the images CFrame, it looks great when observed from a far distance and when moving, only issue is however for some reason when I get close to the tornado or inside of it all the ImageHandleAdornments suddenly face away from the player even though thats not intended.
I always want it to be relatively facing the player’s camera at all times even when close or inside the tornado.
Tried several different solutions, which involved using different CFrames and nothing worked.
Camera code:
local function faceCamera()
for _, image in ipairs(player_gui:GetChildren()) do
if image:IsA("ImageHandleAdornment") then
image.CFrame = CFrame.lookAt(image.CFrame.Position, Vector3.new(cam.CFrame.X, image.CFrame.Position.Y, cam.CFrame.Z));
end;
end;
end;
game:GetService("RunService").PreRender:Connect(function()
faceCamera();
end);
Full code if you need it.
local numParticles = 50 -- number of parts
local riseSpeed = 0.2 -- speed of rise
local swirlSpeed = 5 -- rotation speed
local chaosFactor = 1 -- adds random movments
local folderPart = Instance.new("Model");
folderPart.Name = "TornadoFolder";
folderPart.Parent = workspace;
local cam = workspace.CurrentCamera;
local player = game.Players.LocalPlayer :: Player;
local player_gui = player:WaitForChild("PlayerGui") :: PlayerGui;
local character = player.Character or player.CharacterAdded:Wait();
local definingParts = {};
for _, part in ipairs(script.Parent.TornadoStructure:GetChildren()) do
if part:IsA("BasePart") then
if part:FindFirstChild("TornadoPart").Value == true then
table.insert(definingParts, part);
end;
end;
end;
for _, part in ipairs(script.Parent.TornadoMeshes:GetChildren()) do
if part:IsA("BasePart") then
if part:FindFirstChild("TornadoPart").Value == true then
table.insert(definingParts, part);
end;
end;
end;
table.sort(definingParts, function(a, b)
return a.Position.Y < b.Position.Y;
end);
local function interpolateBetweenAllParts(tProgress)
local numSegments = #definingParts - 1;
local adjustedT = tProgress * numSegments;
local segmentIndex = math.floor(adjustedT);
local localT = adjustedT - segmentIndex;
segmentIndex = math.clamp(segmentIndex, 0, numSegments - 1);
local startPart = definingParts[segmentIndex + 1];
local endPart = definingParts[segmentIndex + 2] or definingParts[#definingParts];
return startPart.Position:Lerp(endPart.Position, localT);
end;
local function calculateRadius(t)
local baseWide = script.Parent.BaseWide.Value;
local topWide = script.Parent.TopWide.Value;
return baseWide + (topWide - baseWide) * t;
end;
local particlesData = {};
local function faceCamera() -- Function that makes all of the tornado images face the players camera.
for _, image in ipairs(player_gui:GetChildren()) do
if image:IsA("ImageHandleAdornment") then
image.CFrame = CFrame.lookAt(image.CFrame.Position, Vector3.new(cam.CFrame.X, image.CFrame.Position.Y, cam.CFrame.Z));
end;
end;
end;
for i = 1, numParticles do
local meshes = script.Parent.TornadoMeshes:GetChildren();
local particle = meshes[math.random(math.max(#meshes, 2))]:Clone();
particle.Parent = folderPart;
local tornado_texture = Instance.new("ImageHandleAdornment")
tornado_texture.Adornee = particle;
tornado_texture.Image = particle:GetAttribute("TornadoTexture");
tornado_texture.Size = Vector2.new(16860, 8856);
tornado_texture.Color3 = Color3.fromRGB(163, 162, 165);
tornado_texture.Parent = player_gui;
table.insert(particlesData, {
particle = particle,
particleProgress = math.random(),
angle = math.random(0, 360),
randomOffset = Vector3.new(
math.random(-chaosFactor, chaosFactor),
0,
math.random(-chaosFactor, chaosFactor)
);
});
end;
local lastUpdateTime = tick()
game:GetService("RunService").Heartbeat:Connect(function()
local deltaTime = tick() - lastUpdateTime;
lastUpdateTime = tick();
for _, particleData in ipairs(particlesData) do
local particle = particleData.particle;
local conePosition = interpolateBetweenAllParts(particleData.particleProgress);
local currentRadius = calculateRadius(particleData.particleProgress);
particleData.angle = particleData.angle - math.rad(swirlSpeed) * deltaTime;
local x = math.cos(particleData.angle) * currentRadius + particleData.randomOffset.X;
local z = math.sin(particleData.angle) * currentRadius + particleData.randomOffset.Z;
particle.Position = conePosition + Vector3.new(x, 0, z);
particleData.particleProgress = particleData.particleProgress + (riseSpeed * 0.01);
if particleData.particleProgress > 1 then
particleData.particleProgress = 0
particleData.randomOffset = Vector3.new(
math.random(-chaosFactor, chaosFactor),
0,
math.random(-chaosFactor, chaosFactor)
)
end;
end;
end);
game:GetService("RunService").PreRender:Connect(function()
faceCamera();
end);