Repeat a pattern on a surface with EditableImage accounted to world position and part rotation

The problem is really hard to describe, so let me show instead:
Animation
As you can see, there’s a pattern which repeats along the whole world, no matter where a part is.
But when I rotate it:
Animation
Arggghhh!!! It just goes wacko mode and nothing aligns anymore!!!

Here’s the script and rblx file:

local Debris = game:GetService("Debris")
local editableImage = Instance.new("EditableImage")
editableImage.Parent = script.Parent.SurfaceGui.ImageLabel
editableImage:Resize(Vector2.new(8,8))

while true do
	task.wait()

	local pixels: {number} = {}
	for x = 0, 7 do
		for y = 0, 7 do
			local pixelWorldPosition = script.Parent.CFrame:PointToWorldSpace(Vector3.new(x/7*4, y/7*4, 0))
			local pixelFactor = (pixelWorldPosition.X + pixelWorldPosition.Y + pixelWorldPosition.Z)/2
			local alpha = math.sin(pixelFactor)
			--print(transparency)
			table.insert(pixels, 1)
			table.insert(pixels, 1)
			table.insert(pixels, 1)
			table.insert(pixels, alpha)
			
			--Uncomment to see pixelWorldPosition
			--local part = Instance.new("Part")
			--part.Anchored = true
			--part.Size = Vector3.new(0.1, 0.1, 0.1)
			--part.Parent = workspace
			--part.Position = pixelWorldPosition
			--Debris:AddItem(part, 0.01)
		end
	end
	--print(pixels)
	--print(#pixels, editableImage.Size.X * editableImage.Size.Y * 4)

	editableImage:WritePixels(Vector2.new(0,0), Vector2.new(8, 8), pixels)
end

Place1.rbxl (55.4 KB)

After a lot of tinkering I´ve found a solution to this problem, or atleast for the front side of a part. I don’t know why it works, but atleast it does.
This tells me that other faces can also have this effect if you just puzzle around with it for long enough.
If anyone can tell me how to use math instead of brute force to solve this problem I´d appreciate that.

local Debris = game:GetService("Debris")
local editableImage = Instance.new("EditableImage")
editableImage.Parent = script.Parent.SurfaceGui.ImageLabel
editableImage:Resize(Vector2.new(8,8))

while true do
	task.wait(0.1)

	local pixels: {number} = {}
	for x = 0, 7 do
		for y = 0, 7 do
			local pixelWorldPosition = script.Parent.CFrame:PointToWorldSpace(Vector3.new(-x/7*4+2, y/7*4-2, -2))
			local pixelFactor = pixelWorldPosition.X + pixelWorldPosition.Y + pixelWorldPosition.Z
			local alpha = math.sin(pixelFactor+tick())
			--print(transparency)
			table.insert(pixels, 1)
			table.insert(pixels, 1)
			table.insert(pixels, 1)
			table.insert(pixels, alpha)
			
			--Uncomment to see pixelWorldPosition
			--local part = game.Workspace.clonepart:Clone()
			--part.Anchored = true
			--part.Size = Vector3.new(0.1, 0.1, 0.1)
			--part.Parent = workspace
			--part.Position = pixelWorldPosition
			--part.Color = Color3.new(x/7, y/7, 0)
			--Debris:AddItem(part, 0.1)
		end
	end
	--print(pixels)
	--print(#pixels, editableImage.Size.X * editableImage.Size.Y * 4)

	editableImage:WritePixels(Vector2.new(0,0), Vector2.new(8, 8), pixels)
end

Here’s a more generalized solution. I used brute force

local Debris = game:GetService("Debris")

local shieldEffectPart = script.Parent

local partSize = shieldEffectPart.Size
local CANVAS_SIZE_PER_STUD = 1

local surfaceGuis: {SurfaceGui} = {
	shieldEffectPart.FrontGui,
	shieldEffectPart.BackGui,
	shieldEffectPart.LeftGui,
	shieldEffectPart.RightGui,
	shieldEffectPart.TopGui,
	shieldEffectPart.BottomGui
}

local function GetWorldOrientedSurface(part: BasePart, normalId: Enum.NormalId): (CFrame, Vector2)
	local cf = part.CFrame
	local rot = cf - cf.Position

	local nObject = Vector3.fromNormalId(normalId)
	local nWorld = rot * nObject

	-- get orthogonal vector by utilizing the order of NormalId enums
	-- i.e. Front.Value is 5 -> (5+1)%6 = 0 -> Right.Value
	local xWorld = rot * Vector3.fromNormalId((normalId.Value + 1) % 6)

	-- get other orthogonal vector
	local zWorld = nWorld:Cross(xWorld)

	-- make them both point "generally down"
	if xWorld.Y > 0 then xWorld = -xWorld end
	if zWorld.Y > 0 then zWorld = -zWorld end

	-- choose the one pointing "more down" one as the z axis for the surface
	if xWorld.Y < zWorld.Y then zWorld = xWorld end

	-- redefine x axis based on that
	xWorld = nWorld:Cross(zWorld)

	local surfaceRot = CFrame.fromMatrix(Vector3.new(), xWorld, nWorld, zWorld)

	-- get width of part in direction of x and y
	local sizeInWorldSpace = rot * part.Size
	local sizeInSurfaceSpace = surfaceRot:Inverse() * sizeInWorldSpace

	-- get position on surface
	local surfaceCFrame = surfaceRot + cf.Position + nWorld * math.abs(sizeInSurfaceSpace.Y) / 2

	return surfaceCFrame, Vector2.new(math.abs(sizeInSurfaceSpace.X), math.abs(sizeInSurfaceSpace.Z))
end


function CalculatePixelWorldPosition(face: Enum.NormalId, pixelPosition: Vector2, editableImage: EditableImage): Vector3
	local _, surfaceSize = GetWorldOrientedSurface(shieldEffectPart, face)
	local pixelObjectPosition: Vector3 = Vector3.new()
	local adjustedPixelPosition = pixelPosition / CANVAS_SIZE_PER_STUD
	
	if face == Enum.NormalId.Front then
		pixelObjectPosition = Vector3.new(-adjustedPixelPosition.X + surfaceSize.X/2, -adjustedPixelPosition.Y + surfaceSize.Y/2, -shieldEffectPart.Size.Z/2)
	elseif face == Enum.NormalId.Back then
		pixelObjectPosition = Vector3.new(adjustedPixelPosition.X - surfaceSize.X/2, -adjustedPixelPosition.Y + surfaceSize.Y/2, shieldEffectPart.Size.Z/2)
	elseif face == Enum.NormalId.Right then
		pixelObjectPosition = Vector3.new(shieldEffectPart.Size.X/2, -adjustedPixelPosition.Y + surfaceSize.Y/2, -adjustedPixelPosition.X + surfaceSize.X/2)
	elseif face == Enum.NormalId.Left then
		pixelObjectPosition = Vector3.new(-shieldEffectPart.Size.X/2, -adjustedPixelPosition.Y + surfaceSize.Y/2, adjustedPixelPosition.X - surfaceSize.X/2)
	elseif face == Enum.NormalId.Top  then
		pixelObjectPosition = Vector3.new(adjustedPixelPosition.Y - surfaceSize.Y/2, shieldEffectPart.Size.Y/2, -adjustedPixelPosition.X + surfaceSize.X/2)
	elseif face == Enum.NormalId.Bottom then
		pixelObjectPosition = Vector3.new(-adjustedPixelPosition.Y + surfaceSize.Y/2, -shieldEffectPart.Size.Y/2, -adjustedPixelPosition.X + surfaceSize.X/2)
	end
	
	return shieldEffectPart.CFrame:PointToWorldSpace(pixelObjectPosition)
end

for _, surfaceGui in surfaceGuis do
	local _, surfaceSize = GetWorldOrientedSurface(shieldEffectPart, surfaceGui.Face)
	local editableImage = Instance.new("EditableImage")
	editableImage.Parent = surfaceGui.ImageLabel
	editableImage:Resize(surfaceSize * Vector2.new(CANVAS_SIZE_PER_STUD, CANVAS_SIZE_PER_STUD))
end

local partCloned = false
while true do
	task.wait()

	for _, surfaceGui in surfaceGuis do
		local editableImage: EditableImage = surfaceGui.ImageLabel.EditableImage
		
		local pixels: {number} = {}
		for y = 1, editableImage.Size.Y do
			for x = 1, editableImage.Size.X do
				local pixelWorldPosition = CalculatePixelWorldPosition(surfaceGui.Face, Vector2.new(x, y), editableImage)
				local tickModulus = tick() % 2048
				local perlinNoiseWorldPos = pixelWorldPosition/5+Vector3.new(tickModulus, tickModulus, tickModulus)
				local pixelFactor = (math.noise(perlinNoiseWorldPos.X, perlinNoiseWorldPos.Y, perlinNoiseWorldPos.Z)+1)/2
				local alpha = math.sin((pixelWorldPosition.X + pixelWorldPosition.Y + pixelWorldPosition.Z) + tick())
				
				--table.insert(pixels, x/editableImage.Size.X)
				--table.insert(pixels, y/editableImage.Size.Y)
				--table.insert(pixels, 0)
				--table.insert(pixels, 1)
				
				table.insert(pixels, 1)
				table.insert(pixels, 1)
				table.insert(pixels, 1)
				table.insert(pixels, alpha)

				--Uncomment to see pixelWorldPosition
				--if not partCloned then
				--	local part = game.Workspace.clonepart:Clone()
				--	part.Anchored = true
				--	part.Size = Vector3.new(0.1, 0.1, 0.1)
				--	part.Parent = workspace
				--	part.Position = pixelWorldPosition
				--	part.Color = Color3.new(x/editableImage.Size.X, y/editableImage.Size.Y, 0)
				--end
			end
		end
		--print(pixels)
		--print(#pixels, editableImage.Size.X * editableImage.Size.Y * 4)

		editableImage:WritePixels(Vector2.new(0,0), editableImage.Size, pixels)
	end
	
	partCloned = true
end

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.