Terrain:ReadVoxels :WriteVoxels :ReplaceMaterial Suddenly error from not being aligned to grid 4/30/2025

I googled this error I have started recieving today from ReadVoxels and WriteVoxels usage in my code and found this Bug Report from last year reporting it and it being fixed. But now it has reappeared. and is breaking everything

This is breaking anything that uses ReadVoxels/WriteVoxels without expanding to a grid.

workspace.Terrain:ReadVoxels(Region3.new(-2,-2,-2),Region3.new(2,2,2), 4)   

In all my years of development in ROBLOX I have not seen an update like this. Is this a bug?
I have gone through my code and updated it with :Expand to grid where necessary but I’m not sure what kind of behavioral or performance drawbacks this has.

After updating my code the ReadVoxels just seems broken to me now

I had this set to Region:ExpandToGrid(4) But it still says Region has to be aligned to the grid.

Expected behavior

Typically :ReadVoxels and :WriteVoxels does not break the code when using any valid region.

Edit: This bug also applies to ReplaceMaterials

This breaks many published things and I will have to update my public resources to fix this issue.

4 Likes

Hi @Magus_ArtStudios, can you please elaborate in which ways is this breaking things? It is supposed to be only a warning message, nothing should be changed in the actual code behavior, and it’s only enabled for Studio for now, while we wait for everyone to fix the problems. If that is not the case, please let us know where so we can fix this. Thanks!
Please be advised that the line you posted, the one which reads from -2 to +2… that’s not aligned to 4. Because 2 % 4 is not 0. Using such ranges means that the code internally snaps it to the closest values that are aligned, but unfortunately this is an old oversight, and the rounding mode is inconsistent and imprecise.
We recommend using ExpandToGrid as we cannot guarantee that the current “accidental” rounding will stay for long. It is not officially supported as you can see in the docs. ExpandToGrid is just a simple rounding calculation and jf you are calling any voxel r/w functions, this should be a negligible overhead.

2 Likes

I have fixed all the issues with my code concerning this, but it caused a ‘stack end’ not just a warning. Before the update, I could input a small region such as -2 ,+2 which equals one voxel the region has x of 4 y of 4 and z 4, Which I thought was correct but I have changed it to a 4 size and it doesn’t throw the warning anymore.


image

I have also read the documentation on these methods and have noticed that it now says Must be snapped to the voxel grid. The issue is workspace.Terrain:FillBlock, :FillCylinder, :FillBall, :FillWedge etc ALL internally snap to the voxel grid so this new behavior is actually somewhat inconsistent with the other Terrain methods. It also breaks a lot of the resources and games that use :ReplaceMaterial, :ReadVoxels, and :WriteVoxels.

In conclusion, this causes a stack end warning that stops the behavior of the code beyond that point. It’s inconsistent with the :FillBlock, :FillWedge, :FillBall,:FillCylinder methods that snap to the grid automatically. I would rather the behavior remained the same becuase now I have some public resources that I have created that I have to update. Then, there are the public resources that won’t be updated by the OP and left in a broken state. Additionally, this situation is almost the same as a bug report last year that was fixed concerning all terrain methods requiring :ExpandToGrid all of a sudden.

1 Like

I’m pretty sure this has changed the behavior of :ReadVoxels because I can’t use it on a 4x4x4 voxel region. Thus it has decreased the accuracy of :ReadVoxels


I just tested it and the smallest region it can expand to with ExpandToGrid(4) is a region that is at least 12x12x12 when before you could Read a much smaller region such as 4x4x4 recieve a small array check the occupancy be done with it. But now you cannot do that… Thus this picture where the boids are stuck getting within 10 units above the terrain when before it could detect a voxel in a 4x4x4 space and go just above the terrain. Because I had to increase the size of the region they detect for terrain they area it detects is larger, the overhead is larger, the algorithm no longer works as intended and there’s nothing I can do about it but change methods to raycast.

Also, the rounding mode present in the Terrain by default should be a feature because I use it a lot and the imprecisity of the rounding it makes it more accurate because if you ExpandToGrid cannot attempt to read just an individual 4x4x4 Voxel, while before it worked with the smallest region being 4x4x4 and now it only works with the smallest region being 16x16x16 or something which is 4 voxels.

My point is this is update has changed the behavior of the API and it no longer works as it did before. The performance overhead is 4x worse because you cannot read a small region of 4x4x4.
I will keep this updated as I further suffer from this update, just please revert it, it’s not consistent with the other terrain:FIll methods and it has changed how useful these apis are since you cannot query an individual voxel anymore for occupancy and you have to check a larger array.

1 Like

“Stack end” just refers to the trace that tells you where the warning is coming from, it’s the same as debug.traceback. It doesn’t stop the Script

2 Likes

Exactly. This is just to make it easier for the developer to find where the code that produces the problem is called from.

Looking at the code, this shouldn’t be so. But of course, I don’t have the same test examples you have. Can you please provide a minimal reproduction sample that shows exact calls and numbers that you are trying to read, and from which terrain, so we can reproduce and debug this?

2 Likes

Well the issue was that ExpandToGrid broke my CheckTerrain function because we can no longer check an individual voxel for occupancy. Via ReadVoxels due to the ExpandToGrid requirement doesn’t work with a 4x4x4 region.

local function detecter(region)
	local material = workspace.Terrain:ReadVoxels(region, 4)   
	local check=false
	local size = material.Size
	local goal={["Water"]=true,["Air"]=true}
	for x = 1, size.X, 1 do
		for y = 1, size.Y, 1 do
			for z = 1, size.Z, 1 do
				if goal[material[x][y][z].Name]==nil then -- == "Water" then
					return true
				end
			end
		end
	end
	return check
end

local function CheckTerrain(GoalPosition)
	local region = Region3.new(GoalPosition-Vector3.new(2,2,2),GoalPosition+Vector3.new(2,2,2)):ExpandToGrid(4)
		while detecter(region) do
			GoalPosition=GoalPosition+Vector3.new(0,2,0)
		region = Region3.new(GoalPosition-Vector3.new(2,2,2),GoalPosition+Vector3.new(2,2,2)):ExpandToGrid(4)
		end	
	return GoalPosition-Vector3.new(0,4,0)--GoalPosition
end

This method worked, and by using a small region you only get a very short array which allows you to quickly check if the space is occupied by terrain.
So, in conclusion this update has made :ReadVoxels less useful for checking an individual voxel for occupancy.
But, I can just use raycasting to do a similar thing. Except it works slightly different because it finds intersections between the ray and the goal position…

local params = RaycastParams.new()
params.FilterDescendantsInstances = {workspace.Terrain}
params.FilterType = Enum.RaycastFilterType.Include
params.IgnoreWater = true

local function setRaycastParams(Params:RaycastParams)
	params=Params
end

local function raycasts(raycastOrigin:Vector3, raycastDirection:Vector3)
	local raycastResult = workspace:Raycast(raycastOrigin, raycastDirection, params)
	if raycastResult then
		return raycastResult.Position
	end
	return raycastOrigin
end
local raycastDirection=Vector3.new(0, -20, 0)
--require(game.ReplicatedStorage.GlobalSpells.Questing.Util.CheckTerrain)
return function (raycastOrigin:Vector3,direction:Vector3,IgnoreWater:boolean)
	if IgnoreWater then params.IgnoreWater=IgnoreWater end 
	return raycasts(raycastOrigin, direction or raycastDirection)
end

The new rquirements has made the regions too large to be useful which is why my function is not working as it did before. I could offset the position by an increment so that I’m still reading a larger region but get the correct y position.
But, using this alternative method by raycasting I know works perfectly, from my more recent projects where I had to perfectly find the surface of the terrain.
I’m going to just convert my check terrain to this function. It might be more performant than my legacy method of checking the terrain with :ReadVoxels.

But this update extends past :ReadVoxels into :WriteVoxels and :ReplaceMaterial.

Even though it was not aligned to 4 it should still work because the size of the chunk is exactly 4, and a 4x4x4 region doesn’t work with expand to grid if the Region is for example Region3.new(Vector3.new(-2,-2,-2),Vector3.new(2,2,2)) yet it would work Region3.new(Vector3.new(0,0,0),Vector3.new(4,4,4)) because that is snapped perfectly to the grid? That isn’t very robust with a dynamic position. Yet it works if you have a Region that is twice as large Region3.new(Vector3.new(-4,-4,-4),Vector3.new(4,4,4))? So you would have to snap the original positions used to make the region to a 4x4x4 grid before creating a 4x4x4 region?
My point is it’s not very consistent. the old behavior was fine because it’s still used for the terrain via :FIllBlock, :FillCylinder, :FillWedge, :FillBall, etc this new behavior concerning just :ReadVoxels, :WriteVoxels and :ReplaceMaterial is inconsistent.

It felt like more of a feature, it’s been around for over 8 years. That being said I have replaced my old problem function with a drop in module that does a raycast instead. So I’m more or less over it. But I still don’t think it’s consistent with the Terrain:Fill methods besides them not using a Region3.

But this new solution using raycast is a lot more efficient and accurate. All of my buildings and trees, plants are placed perfectly on top of the terrain.

.

Ok, I will mark this as resolved then.

For posterity here is the technical explanation of what happens:

The docs explicitly say:

Must be aligned to the voxel grid

The fact that this wasn’t properly enforced was a bug. It just let the input “fall through” while throwing errors about numeric incorrectness internally, which were not surfaced to the API interface. As we are now clearing up those cases, we have to start enforcing this.

The fact that it “mostly worked ok” is pure coincidence, and your code was probably relying on exactly how wrong that worked, but that’s not a guarantee can keep. Hence this warning.

As for exact numeric errors the -2 .. +2 centered case, if the center is 16, it gives 14 .. 18 and that actually converts to 12 .. 16. Which gives you one voxel, but it’s off-center from where you though you’d get.

That kind of “shifts” your entire “world model” is by half a voxel. But that’s not the worst part of it. The worst part is that the negative numbers are converted with “towards zero”, so if your center is -16, you get -16 .. -12. So it’s shifted in different directions in different octants.

And in the end - the really worst part of it is that, around 0, you can get an empty box. Because -2 .. +2 literally converts to 0…0.

Expand to grid does correct expansion, meaning that the minimum corner of the box snaps downwards (Math.floor()) to the nearest voxel edge, and maximum corner snaps upwards (Math.ceil()). So -2 .. +2 snaps to -4 .. +4. so you get 2 voxels instead of 1. But it is centered where you asked for, and you will never get an empty box (unless you pass in an empty region - where min == max literally).

This has nothing to do with functions like FillBall() - for a simple reason: those don’t operate on whole voxels. Your radius can literally be 10.25 and that gives a different ball than 10.0. Or even the coordinates can be non-integer. This warning only applies to functions that operate on whole voxels by manipulating their values directly.

I hope this explains all the details.

2 Likes

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