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

I’ve seen some games with 3D blocks that have a blurry texture. Now I’ve worked on some games that include 3D blocks with pixel textures and I also ran into this problem, this was a while ago but I’d like to share the method of getting pixel perfect textures which a more experienced developer might know but less experienced developers might not.


Here’s an example of the three display types, with the last two being UI and Mesh.

To achieve this, add a surface UI to any part in your game, then adding the Image UI element and going to its properties and navigate to image category changing “ResampleMode” to “Pixelated” this will create the effect. After this you can change the surface UI parent Part to be neon and then black.

For the mesh, what you want to do is bit more complex.
In my case I use blender to make the 3D model.


As you can see the mesh is not just a simple cube but
it’s made of a bunch of little squares, each square corresponding to a pixel on the UV. I lost the original post where I saw the mesh technique was shared and used.

Anyways, I thought it’d just be neat to share this to help:>

“Some extra context I forgot to add while posting this”

‘Pros’ and ‘Cons’ to the UI and the Mesh technique, well some.

UI Pros = {
Basically pixel perfect,
Lighting customizability,
No “posterization”/chunky colors
}

UI Cons = {
“Annoying to keep track”,
"Doesn’t display in ‘Viewport Frames’ "
}

Mesh Pros = {
"Displays in ‘Viewport Frames’ ",
}

Mesh Cons = {
“Annoying to setup with out automation”,
“With too low of a texture size Roblox will auto “compress” the data of the texture on the mesh causing chunky colors
involving the upload of under 256 size images, really like 255”
}

2 Likes

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

4 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.

4 Likes

In short, yes and no, While multiple game engines auto convert any squares/edges into triangles for the GPU, optimization methods can be used. The Roblox engine is also quite efficient at having multiple objects in a scene.

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?

You’re right, using 6 faces // 12 triangles per cube is simpler. The reason for using more triangles is to achieve a pixel-perfect look, where each “pixel” of the texture maps to a separate face. This method just ensures an almost perfect non-blurry, with also not uploading a upscaled pixel-decal. It’s kind of just a niche level of detail if the player is right up-close to the object.


I favor the UI approach more, this means the mesh technique is more useless but it’s used when UI can’t be displayed.

How do the files get uploaded to use the script?

1 Like