Cool pixel-perfect/nearest-neighbor/pixel-perfect 3D objects

Rewritten

To start this off, I’ve seen some “blocky” games with this
texture blur problem, and I want to share my experience with how I personally went about solving this.


This is an example of four blocks I’ve created, each with their own method of displaying a “texture”.

The two cubes on the right display the outcome we want, each one having their pros and cons.

Part -> SurfaceGui -> ImageLabel -> Properties-ResampleMode -> "Pixelated"

For the Surface UI method it’s actually pretty easy. All you have to do is take a part !preferably a cube!, and add the instance SurfaceGui, after that you can assign it a face. After this you simply want to add an ImageLabel instance under the SurfaceGui instance. “Edit the ImageLabel to preference”. The key property to edit is the ResampleMode property within the ImageLabel, simply change it to “Pixelated”.

"Also I recommend a transparency of 0 for the parent part to prevent Z-Fighting"

The only con I’ve run into with the SurfaceGui method is that the part doesn’t display properly within ViewportFrames. !Not a bug!

Although the pro of using SurfaceGui is that the corners of each pixel are crisp and sharp. If that is the style you want to achieve.

As for the Mesh UV method, the pros of using a mesh is that you can display it via ViewportFrames and might be softer on your eyes because of MSAA I assume.


I’ve seen other developers take a mesh and structure the UV like the example image. !Please ignore the random edges!

The cons of using Mesh UV is that you increase face count at an alarming rate for open-world games, and that MSAA!I assume! might just get in the way of your preferred style. The last con I forgot to add states that you may need to upload larger images for mesh texture due to Roblox tendency to compress the color on smaller image files.

Have a nice day.

3 Likes

But won’t this be useless triangles in the game? Increasing the performance load exponentially?

7 Likes

Pretty cool, but this approach limits you to blocks or blocky meshes. It’s also not the most performant way of achieving this. IMO, a better way to get nearly pixel perfect textures, without unnecessarily uploading 1024x1024 images, is to import and upscale them using editable images.

Here's a script that does this with image labels. But it can be applied to mesh part textures as well.
local assetId = "rbxassetid://100362955533266"; -- change the asset id here

-- If target res is a Vector2, the function will stretch the output image to fit the desired resolution
-- If target res is a number, it will stretch the longest side of the image and keep the aspect ratio
local function NearestNeighbour(imageIn:EditableImage, targetRes:Vector2|number)
	local sizeIn = imageIn.Size;
	local sizeOut; do
		if (typeof(targetRes)=="Vector2") then
			sizeOut = targetRes;
		elseif (sizeIn.X > sizeIn.Y) then
			sizeOut = Vector2.new(targetRes, sizeIn.Y/sizeIn.X * targetRes);
		else
			sizeOut = Vector2.new(sizeIn.X/sizeIn.Y * targetRes, targetRes);
		end
	end
	local sizeInX = sizeIn.X;
	local sizeInY = sizeIn.Y;
	local sizeOutX = sizeOut.X;
	local sizeOutY = sizeOut.Y;
	local _ = (sizeOutX < 0 or sizeOutY < 0 or sizeOutX > 1024 or sizeOutY > 1024) and error("Invalid target resolution "..sizeOutX.." x "..sizeOutY);
	local bufferIn = imageIn:ReadPixelsBuffer(Vector2.zero, sizeIn);
	local bufferOut = buffer.create(sizeOutX*sizeOutY*4);
	local scale = sizeOut/sizeIn;
	local scaleX = scale.X;
	local scaleY = scale.Y;

	for yIn = 0, sizeInY-1 do
		for xIn = 0, sizeInX-1 do
			local color = buffer.readi32(bufferIn, (yIn*sizeInX+xIn)*4);
			for yOut = math.max(math.round(yIn*scaleY), 0), math.min(math.round((yIn+1)*scaleY), sizeOutY)-1 do
				for xOut = math.max(math.round(xIn*scaleX), 0), math.min(math.round((xIn+1)*scaleX), sizeOutX)-1 do
					buffer.writei32(bufferOut, (yOut*sizeOutX+xOut)*4, color);
				end
			end
		end
	end

	local imageOut = AssetService:CreateEditableImage({Size = sizeOut});
	imageOut:WritePixelsBuffer(Vector2.zero, sizeOut, bufferOut);
	return imageOut;
end

-- Load the asset id into an editable image and upscale it to 1024 pixels
local imageIn = game:GetService("AssetService"):CreateEditableImageAsync(assetId);
local imageOut = NearestNeighbour(imageIn, 1024);
local sizeIn = imageIn.Size;
local sizeOut = imageOut.Size;

print(sizeIn)
print(sizeOut)

-- Apply these images to a gui
local sgui = Instance.new("ScreenGui");

local labelIn = Instance.new("ImageLabel");
labelIn.Size = UDim2.fromOffset(500, 500);
labelIn.ZIndex = 10;
labelIn.AnchorPoint = Vector2.zero;
labelIn.ImageContent = Content.fromObject(imageIn);
labelIn.Parent = sgui;

local labelOut = Instance.new("ImageLabel");
labelOut.Size = UDim2.fromOffset(500, 500);
labelOut.Position = UDim2.fromScale(1,0);
labelOut.AnchorPoint = Vector2.new(1,0);
labelOut.ImageContent = Content.fromObject(imageOut);
labelOut.Parent = sgui;

sgui.Parent = game:GetService("Players").LocalPlayer:WaitForChild("PlayerGui");

EDIT: Expanded this script by a lot. This idea made me want to write an entire nearest neighbour implementation, so I edited this post with the result.

5 Likes

Huh, neat. Seems like a great way to handle pixel textures without uploading huge files. I messed around with it a bit:>.

1 Like

Yes but why not just keep 6 faces or 12 triangles per cube rather than having thousands? Why won’t it work? You’re just applying a texture no?

How do the files get uploaded to use the script?

1 Like