Hello, I tried making this blood splatter system. For the most part it works, but the part where if finds the face dimensions and offset doesn’t seem to work for all rotations. I am too inexpierienced in this to figure out how to do it properly, any help it appreciated.
-- blood_splatter_module.lua
local module = {}
-- Services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
-- Modules
local Modules = ReplicatedStorage:WaitForChild("Modules")
local BothModules = Modules:WaitForChild("Both")
local UtilitiesModules= BothModules:WaitForChild("Utilities")
-- Assets
local Assets = ReplicatedStorage:WaitForChild("Assets")
local Decals = Assets:WaitForChild("Decals")
local Blood = Decals:WaitForChild("Blood")
local BloodStains = Blood:WaitForChild("BloodStains")
local BloodSplatters= BloodStains:WaitForChild("Splatters")
local Utilities = require(UtilitiesModules)
-- Helper to rotate a direction vector down by given degrees
local function rotateDirectionDown(vector, degrees)
local radians = math.rad(degrees)
local right = vector:Cross(Vector3.new(0, 1, 0)).Unit
if right.Magnitude == 0 then
right = Vector3.new(1, 0, 0)
end
return CFrame.fromAxisAngle(right, radians):VectorToWorldSpace(vector)
end
local function approxEquals(v1, v2, epsilon)
return (v1 - v2).Magnitude < epsilon
end
local function getClosestFace(result)
local normal = result.Normal
local part = result.Instance
-- Convert world normal to part local space
local localNormal = part.CFrame:VectorToObjectSpace(normal)
-- Determine which face it hit
local HitFace = Enum.NormalId.Front
local epsilon = 0.001 -- small tolerance
if approxEquals(localNormal, Vector3.new(0, 1, 0), epsilon) then
HitFace = Enum.NormalId.Top
elseif approxEquals(localNormal, Vector3.new(0, -1, 0), epsilon) then
HitFace = Enum.NormalId.Bottom
elseif approxEquals(localNormal, Vector3.new(1, 0, 0), epsilon) then
HitFace = Enum.NormalId.Right
elseif approxEquals(localNormal, Vector3.new(-1, 0, 0), epsilon) then
HitFace = Enum.NormalId.Left
elseif approxEquals(localNormal, Vector3.new(0, 0, -1), epsilon) then
HitFace = Enum.NormalId.Back
elseif approxEquals(localNormal, Vector3.new(0, 0, 1), epsilon) then
HitFace = Enum.NormalId.Front
end
return HitFace
end
-- Main blood splatter function
-- Start: origin position
-- Direction: look vector to raycast
-- Range: max distance
module.BloodSplatter = function(Start, Direction, Range)
-- Attempt direct raycast then angled downwards
local hit, raycastResult = Utilities.CheckLocation(Start, nil, nil, Direction * Range)
if not hit then
local downDir = rotateDirectionDown(Direction, 45)
hit, raycastResult = Utilities.CheckLocation(Start, nil, nil, downDir * Range)
end
if not hit then return end
local part = raycastResult.Instance :: Part
local hitPos = raycastResult.Position
local hitNormal= raycastResult.Normal
if not part or not hitPos or not hitNormal then return end
-- Spawn splatter
local template = BloodSplatters:GetChildren()
local randomTempl = template[math.random(1, #template)]
local splat = randomTempl:Clone() :: Part
splat.CFrame = CFrame.new(hitPos,hitPos + hitNormal)
local size = part.Size
local position = part.Position
local FaceSize
local Offset = part.CFrame:ToObjectSpace(splat.CFrame)
local Offset2D = Vector2.new()
local HitFace = getClosestFace(raycastResult)
if HitFace == Enum.NormalId.Top or HitFace == Enum.NormalId.Bottom then
FaceSize = Vector2.new(size.X, size.Z)
Offset2D = Vector2.new(Offset.X,Offset.Z)
elseif HitFace == Enum.NormalId.Front or HitFace == Enum.NormalId.Back then
FaceSize = Vector2.new(size.X, size.Y)
Offset2D = Vector2.new(Offset.X,Offset.Y)
elseif HitFace == Enum.NormalId.Left or HitFace == Enum.NormalId.Right then
FaceSize = Vector2.new(size.Z, size.Y)
Offset2D = Vector2.new(Offset.Z,Offset.Y)
end
local AbsoluteOffset = Vector2.new(math.abs(Offset2D.X),math.abs(Offset2D.Y))
local SplatSize2D = Vector2.new(splat.Size.X,splat.Size.Y)
local Overlap = (SplatSize2D - FaceSize) / 2 + AbsoluteOffset
Overlap = Vector2.new(math.clamp(Overlap.X,0,splat.Size.X),math.clamp(Overlap.Y,0,splat.Size.Y))
local ClampX = math.clamp(splat.Size.X - Overlap.X,0,FaceSize.X)
local ClampY = math.clamp(splat.Size.Y - Overlap.Y,0,FaceSize.Y)
splat.Size = Vector3.new(ClampX,ClampY,0)
print(Offset2D.X,Offset2D.Y)
if AbsoluteOffset.X > 0 then
if Offset2D.X < 0 then
splat.CFrame = splat.CFrame * CFrame.new(-Overlap.X / 2,0,0)
else
splat.CFrame = splat.CFrame * CFrame.new(Overlap.X / 2,0,0)
end
end
if AbsoluteOffset.Y > 0 then
if Offset2D.Y < 0 then
splat.CFrame = splat.CFrame * CFrame.new(0,Overlap.Y / 2,0)
else
splat.CFrame = splat.CFrame * CFrame.new(0,-Overlap.Y / 2,0)
end
end
splat.Parent = workspace.Effects
task.delay(60,function()
splat:Destroy()
end)
end
return module