This is possible! However, achieving this effect requires a decent knowledge of how the Terrain system works on Roblox. I might write a more in-depth explanation later, but basically you want to create a shell of low occupancy voxels (Occupancy = 1 / 256) around a volume of full occupancy voxels (Occupancy = 1).
The images below show a 4x3x4 region of voxels with the voxel boundary box in black. The green cube in the 2nd image shows the bounding box of a single voxel, and the third image shows a texture on the surface of the bounding box representing the outer faces of the voxels.
This technique can be applied to most terrain-compatible material, however depending on the geometry of the material, results may vary. The Material Manager allows custom materials to choose the “Pattern” which is a reference to a Enum.TerrainPattern
type, however there’s seemingly missing EnumItems since terrain Water appears to be a unique case and renders lower (roughly half a voxel) than other materials with identical occupancy data. Some materials, like WoodPlanks, can only render as completely full voxels or completely empty voxels.
Anyways, here’s some code you can use to generate “sharp terrain cubes” like the images shown above. I didn’t add checks for ensuring the position and size of the region are aligned to the voxel grid, but if you pass in the Position and Size of a Part that is aligned to the voxel grid it should work fine:
The code:
--!strict
--Helper function to generate 3D arrays used when calling `Terrain:WriteVoxels()`
local function makeVoxelArray<T>(size: Vector3, v: T): {{{T}}}
local array = {}
for x = 1, size.X do
array[x] = {}
for y = 1, size.Y do
array[x][y] = {}
for z = 1, size.Z do
array[x][y][z] = v
end
end
end
return array
end
--This function will fill a region with a "sharp" cube of terrain voxels
--The bounding box of the `size` parameter (centered on `centerPos`) *should* be aligned to the voxel grid (aka it's corners should be on voxel corners)
function fillSharpTerrainCube(centerPos: Vector3, size: Vector3, material: Enum.Material)
local voxelSize: Vector3 = size / 4
local corner1: Vector3 = centerPos - 2 * voxelSize
local corner2: Vector3 = centerPos + 2 * voxelSize
local region: Region3 = Region3.new(
Vector3.new(
math.min(corner1.X, corner2.X),
math.min(corner1.Y, corner2.Y),
math.min(corner1.Z, corner2.Z)
),
Vector3.new(
math.max(corner1.X, corner2.X),
math.max(corner1.Y, corner2.Y),
math.max(corner1.Z, corner2.Z)
)
)
local matArray = makeVoxelArray(voxelSize, material)
local occArray = makeVoxelArray(voxelSize, 1)
for x = 1, voxelSize.X do
for y = 1, voxelSize.Y do
for z = 1, voxelSize.Z do
local isEdgeX: boolean = x == 1 or x == voxelSize.X
local isEdgeY: boolean = y == 1 or y == voxelSize.Y
local isEdgeZ: boolean = z == 1 or z == voxelSize.Z
if isEdgeX or isEdgeY or isEdgeZ then
occArray[x][y][z] = 1 / 256
end
end
end
end
workspace.Terrain:WriteVoxels(region, 4, matArray, occArray)
end