already possible, i made this with ai 2 months ago in like 3 hours
Def haha,
I am kinda mad rn tho ![]()
I have to recreate the whole topology system. Im hoping this will help.
These beta features can be used in real games, right?
nope! only in studio, once they get released you can put them in game
Yes, but your current(old) version does not support dynamic triangles.
New “Quad Support” does.
From my experience so far, I can confirm that this is working perfectly.
I’ve integrated this new feature into my Triangle-based Editor and tested it very carefully. So far, I haven’t encountered any issues after 20+ tests, including tests with different topology flows.
Based on these results, I think the feature is ready to move out of beta and be published as a fully supported, production-ready feature.
Honestly, I don’t even see a reason for this to still be a [Beta] feature.
![]()
How I tested it
- Multiple Triangle/Quad-based topology setups, including manually faked Ngons
- An actual UV unwrapping and texture-painting workflow. (Not unwrapping the Quad as 2 different triangles. Unwrapping a actualy Quad as 4 Vertexs)
- Various topology flows and edge cases
- Direct integration into my Triangle-based Editor, going from fully manual triangles → dynamic quads
Previous Version
Old version video here (it’s also included in the post attachment lol):
New Version
New version as a proof of concept:
As I also said above I’ve tested this with more complicated topology too, not with just a basic cube.
I also made a small module to simplify the script organization and make quad usage a little cleaner while supporting the removal of the quad:
local QuadMesh = {}
function QuadMesh.AddQuad(editableMesh, v1, v2, v3, v4)
local faceId = editableMesh:AddFace({
v1,
v2,
v3,
v4
})
if not faceId then return end
return {
vertices = {v1, v2, v3, v4},
faces = {faceId},
faceId = faceId
}
end
function QuadMesh.RemoveQuad(editableMesh, quad)
if not quad then return end
for _, faceId in ipairs(quad.faces) do
editableMesh:RemoveFace(faceId)
end
end
return QuadMesh
Btw I would be happy to answer any questions
Here is also a rotating tesseract model I’ve tried to make. Def 100% wrong on the model. But looks like the Quads are working correct.
local AssetService = game:GetService("AssetService")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local Camera = workspace.CurrentCamera
local SIZE = 4
local PROJECTION_DISTANCE = 8
local PROJECTION_SCALE = 5
local POSITION = Vector3.new(0, 6, 0)
local ROTATION_XW_SPEED = 0.18
local ROTATION_YW_SPEED = 0.12
local ROTATION_ZW_SPEED = 0.08
local CAMERA_DISTANCE = 14
local CAMERA_MIN_DISTANCE = 6
local CAMERA_MAX_DISTANCE = 30
local CAMERA_SENSITIVITY = 0.35
local CAMERA_MIN_PITCH = math.rad(-80)
local CAMERA_MAX_PITCH = math.rad(80)
local EDGE_WIDTH = 0.06
local EDGE_COLOR = Color3.fromRGB(20, 20, 20)
local editableMesh = AssetService:CreateEditableMesh()
local points = {}
local vertexIds = {}
local faces = {}
local projectedPositions = {}
local edges = {}
local cameraYaw = math.rad(45)
local cameraPitch = math.rad(20)
local cameraDistance = CAMERA_DISTANCE
local rotatingCamera = false
local visibleColor = editableMesh:AddColor(Color3.fromRGB(80, 170, 255), 0)
local hiddenColor = editableMesh:AddColor(Color3.fromRGB(80, 170, 255), 1)
local edgeFolder = Instance.new("Folder")
edgeFolder.Name = "4D Cube Edges"
edgeFolder.Parent = workspace
local function GetIndex(x, y, z, w)
local xIndex = x == 1 and 1 or 0
local yIndex = y == 1 and 1 or 0
local zIndex = z == 1 and 1 or 0
local wIndex = w == 1 and 1 or 0
return xIndex * 8 + yIndex * 4 + zIndex * 2 + wIndex + 1
end
local function RotatePlane(a, b, angle)
local cosine = math.cos(angle)
local sine = math.sin(angle)
return a * cosine - b * sine, a * sine + b * cosine
end
local function Project(point, time)
local x = point.x
local y = point.y
local z = point.z
local w = point.w
x, w = RotatePlane(x, w, time * ROTATION_XW_SPEED)
y, w = RotatePlane(y, w, time * ROTATION_YW_SPEED)
z, w = RotatePlane(z, w, time * ROTATION_ZW_SPEED)
local scale = PROJECTION_SCALE / (PROJECTION_DISTANCE - w)
return Vector3.new(
x * scale,
y * scale,
z * scale
)
end
local function CalculateQuadNormal(vertexA, vertexB, vertexC, vertexD)
local positionA = editableMesh:GetPosition(vertexA)
local positionB = editableMesh:GetPosition(vertexB)
local positionC = editableMesh:GetPosition(vertexC)
local positionD = editableMesh:GetPosition(vertexD)
local firstCross = (positionB - positionA):Cross(positionC - positionA)
local secondCross = (positionC - positionA):Cross(positionD - positionA)
local normal = firstCross + secondCross
if normal.Magnitude < 1e-6 then return nil end
return normal.Unit
end
for x = -1, 1, 2 do
for y = -1, 1, 2 do
for z = -1, 1, 2 do
for w = -1, 1, 2 do
local index = GetIndex(x, y, z, w)
points[index] = {
x = x * SIZE * 0.5,
y = y * SIZE * 0.5,
z = z * SIZE * 0.5,
w = w * SIZE * 0.5
}
local position = Project(points[index], 0)
projectedPositions[index] = position
vertexIds[index] = editableMesh:AddVertex(position)
end
end
end
end
local function AddQuad(a, b, c, d, innerFace)
local positionA = editableMesh:GetPosition(a)
local positionB = editableMesh:GetPosition(b)
local positionC = editableMesh:GetPosition(c)
local positionD = editableMesh:GetPosition(d)
local normal = CalculateQuadNormal(a, b, c, d)
if normal then
local faceCenter = (positionA + positionB + positionC + positionD) / 4
local desiredDirection = innerFace and -faceCenter or faceCenter
if normal:Dot(desiredDirection) < 0 then
b, d = d, b
normal = CalculateQuadNormal(a, b, c, d)
end
end
local faceId = editableMesh:AddFace({
a,
b,
c,
d
})
local normalId = editableMesh:AddNormal(normal or Vector3.yAxis)
editableMesh:SetFaceNormals(faceId, {
normalId,
normalId,
normalId,
normalId
})
editableMesh:SetFaceColors(faceId, {
visibleColor,
visibleColor,
visibleColor,
visibleColor
})
table.insert(faces, {
faceId = faceId,
normalId = normalId,
vertexA = a,
vertexB = b,
vertexC = c,
vertexD = d
})
end
for dimensionA = 1, 3 do
for dimensionB = dimensionA + 1, 4 do
local fixedDimensions = {}
for dimension = 1, 4 do
if dimension ~= dimensionA and dimension ~= dimensionB then
table.insert(fixedDimensions, dimension)
end
end
local variesW = dimensionA == 4 or dimensionB == 4
for fixedA = -1, 1, 2 do
for fixedB = -1, 1, 2 do
local values = {0, 0, 0, 0}
values[fixedDimensions[1]] = fixedA
values[fixedDimensions[2]] = fixedB
values[dimensionA] = -1
values[dimensionB] = -1
local indexA = GetIndex(values[1], values[2], values[3], values[4])
values[dimensionA] = 1
values[dimensionB] = -1
local indexB = GetIndex(values[1], values[2], values[3], values[4])
values[dimensionA] = 1
values[dimensionB] = 1
local indexC = GetIndex(values[1], values[2], values[3], values[4])
values[dimensionA] = -1
values[dimensionB] = 1
local indexD = GetIndex(values[1], values[2], values[3], values[4])
local innerFace = not variesW and values[4] == -1
AddQuad(
vertexIds[indexA],
vertexIds[indexB],
vertexIds[indexC],
vertexIds[indexD],
innerFace
)
end
end
end
end
local function AddEdge(indexA, indexB)
local edgePart = Instance.new("Part")
edgePart.Name = "Edge"
edgePart.Anchored = true
edgePart.CanCollide = false
edgePart.CanTouch = false
edgePart.CanQuery = false
edgePart.CastShadow = false
edgePart.Material = Enum.Material.Neon
edgePart.Color = EDGE_COLOR
edgePart.Size = Vector3.new(EDGE_WIDTH, EDGE_WIDTH, 1)
edgePart.Parent = edgeFolder
table.insert(edges, {
part = edgePart,
indexA = indexA,
indexB = indexB
})
end
for index = 1, 16 do
local x = ((index - 1) // 8) % 2 == 1 and 1 or -1
local y = ((index - 1) // 4) % 2 == 1 and 1 or -1
local z = ((index - 1) // 2) % 2 == 1 and 1 or -1
local w = (index - 1) % 2 == 1 and 1 or -1
local values = {x, y, z, w}
for dimension = 1, 4 do
if values[dimension] == -1 then
local other = {x, y, z, w}
other[dimension] = 1
local otherIndex = GetIndex(
other[1],
other[2],
other[3],
other[4]
)
AddEdge(index, otherIndex)
end
end
end
local meshPart = AssetService:CreateMeshPartAsync(Content.fromObject(editableMesh))
meshPart.Name = "4D Cube"
meshPart.Anchored = true
meshPart.CanCollide = false
meshPart.CastShadow = false
meshPart.DoubleSided = true
meshPart.CFrame = CFrame.new(POSITION)
meshPart.Material = Enum.Material.SmoothPlastic
meshPart.Parent = workspace
local function UpdateFace(face)
local normal = CalculateQuadNormal(
face.vertexA,
face.vertexB,
face.vertexC,
face.vertexD
)
if not normal then return end
editableMesh:SetNormal(face.normalId, normal)
local positionA = editableMesh:GetPosition(face.vertexA)
local positionB = editableMesh:GetPosition(face.vertexB)
local positionC = editableMesh:GetPosition(face.vertexC)
local positionD = editableMesh:GetPosition(face.vertexD)
local faceCenter = (positionA + positionB + positionC + positionD) / 4
local cameraPosition = meshPart.CFrame:PointToObjectSpace(Camera.CFrame.Position)
local directionToCamera = cameraPosition - faceCenter
if directionToCamera.Magnitude < 1e-6 then return end
local visible = normal:Dot(directionToCamera.Unit) > 0
editableMesh:SetFaceColors(face.faceId, {
visible and visibleColor or hiddenColor,
visible and visibleColor or hiddenColor,
visible and visibleColor or hiddenColor,
visible and visibleColor or hiddenColor
})
end
local function UpdateEdge(edge)
local positionA = projectedPositions[edge.indexA] + POSITION
local positionB = projectedPositions[edge.indexB] + POSITION
local direction = positionB - positionA
local length = direction.Magnitude
if length < 1e-6 then
edge.part.Transparency = 1
return
end
edge.part.Transparency = 0
local midpoint = (positionA + positionB) * 0.5
edge.part.Size = Vector3.new(
EDGE_WIDTH,
EDGE_WIDTH,
length
)
edge.part.CFrame = CFrame.lookAt(midpoint, positionB)
end
UserInputService.InputBegan:Connect(function(input, processed)
if processed then return end
if input.UserInputType ~= Enum.UserInputType.MouseButton2 then return end
rotatingCamera = true
UserInputService.MouseBehavior = Enum.MouseBehavior.LockCurrentPosition
end)
UserInputService.InputEnded:Connect(function(input)
if input.UserInputType ~= Enum.UserInputType.MouseButton2 then return end
rotatingCamera = false
UserInputService.MouseBehavior = Enum.MouseBehavior.Default
end)
UserInputService.InputChanged:Connect(function(input, processed)
if processed then return end
if input.UserInputType ~= Enum.UserInputType.MouseWheel then return end
cameraDistance -= input.Position.Z * 2
cameraDistance = math.clamp(cameraDistance, CAMERA_MIN_DISTANCE, CAMERA_MAX_DISTANCE)
end)
UserInputService.InputChanged:Connect(function(input)
if not rotatingCamera then return end
if input.UserInputType ~= Enum.UserInputType.MouseMovement then return end
cameraYaw -= input.Delta.X * CAMERA_SENSITIVITY * 0.01
cameraPitch -= input.Delta.Y * CAMERA_SENSITIVITY * 0.01
cameraPitch = math.clamp(cameraPitch, CAMERA_MIN_PITCH, CAMERA_MAX_PITCH)
end)
Camera.CameraType = Enum.CameraType.Scriptable
RunService.RenderStepped:Connect(function()
local offset = CFrame.fromEulerAnglesYXZ(cameraPitch, cameraYaw, 0).LookVector * -cameraDistance
Camera.CFrame = CFrame.lookAt(
POSITION + offset,
POSITION
)
end)
RunService.Heartbeat:Connect(function()
local time = os.clock()
for index, point in pairs(points) do
local position = Project(point, time)
projectedPositions[index] = position
editableMesh:SetPosition(
vertexIds[index],
position
)
end
for _, edge in edges do
UpdateEdge(edge)
end
for _, face in faces do
UpdateFace(face)
end
end)
You don’t need “modules” or “utilities” (bloat)
You need knowledge and tutorials
Haven’t looked into EditableMesh for a while maybe in the future will make a tutorial about them (maybe)
Cool, Noted. More better buildings.
Are there any future plans to support an arbitrary number of vertices per face?
When is this feature expected to go out of Beta?
Blender in Roblox is all we wanted!
Im having issues with lag when it comes to updating the cframe of a meshpart that uses bones. The script im using:
local mesh = CreateSphere(Configuration.Subdivisions, false)
local meshBone = mesh:AddBone({Name = "Main", CFrame = CFrame.new(), Virtual = false})
for _, vertexId in mesh:GetVertices() do
mesh:SetVertexBones(vertexId, {meshBone})
mesh:SetVertexBoneWeights(vertexId, {1})
end
local _, result = AssetService:CreateDataModelContentAsync(Content.fromObject(mesh))
mesh:Destroy()
local meshpart = AssetService:CreateMeshPartAsync(result, MESHPART_PARAMS)
meshpart:ApplyMesh(meshpart)
meshpart.Transparency = Configuration.Transparency
meshpart.Reflectance = Configuration.Reflectance
meshpart.Material = Configuration.Material
meshpart.EnableFluidForces = false
meshpart.CanCollide = false
meshpart.CanQuery = false
meshpart.CanTouch = false
meshpart.Anchored = true
meshpart.Name = "Particle"
local bone = Instance.new("Bone")
bone.Name = "Main"
bone.Parent = meshpart
the more geometry the mesh has, the more lag it produces when moving the meshpart around. Removing the bone instance and bone setup in the mesh, completely fixes the issue
Not sure how this slipped my radar, haha. This is a massive step forward for the EditableMesh API! Having native quad support should unlock some real possibilities. I’m gonna shamelessly advertise my plugin here…
For anyone interested, I’m putting some finishing touches on Meshie before it’s alpha-release, a full-scale mesh editor plugin I’ve been developing for almost a year now. Meshie features a custom radial edge structure leveraged off EditableMesh that handles Ngons, as well as an ever-growing toolkit of mesh-manipulating operations and in-studio texture editing (coming very soon). Needless to say I’m pretty stoked to incorporate quad support!
Another improvement I can’t wait to be released
pls releaz
Unsure if this thread is still being monitored but could we get more ways to modify the way emeshes are rendered? I was hoping to use the Emesh to render Quad and Pyramid particles for some custom smoke for my game but I found that emeshes use a more precise and expensive render pipeline on higher graphics settings than native engine particles which was something I have no control over. While the results were visually pleasing in game, the performance was about an order of magnitude worse for overdraw which was unacceptable. I understand that this was probably not the intended purpose of the Emesh but since we are moving in the direction of supporting quads maybe this is something that can be at least looked into? At low graphics settings the emesh renders with voxelized lighting which actually performs well but thats ONLY if the user drops all of their settings.
As of now, In order to render my physics based particles I am stuck using 1 particle emitter per particle which is 1 scene draw call per particle! I found that compromising on draw calls is more performant than trying to use Emeshes for particles which is a shame. AFAIK there isnt any way of rendering a billboard that is both instanced properly and uses the same internal rendering paths of particles. It would be extremely helpful for me If I could render the emesh the same way particles are rendered or just expose a API to actually allow me to move particles.
Emesh on low graphics settings, renders like normal particles
Emesh on high graphics settings, overdraw nightmare that kills fps when you step foot in it.
Have you tried using CreateDataModelContentAsync for your use case? It allows you to bake Editable* content down into a more performant static version of the content that allows the engine to manage scalability better.
Editable* Objects have to maintain all of the Content in memory (e.g. verts / pixel data) in case a Luau script accesses / modifies it. So it is really designed for cases where you are actively creating / modifying a mesh or an image. When you are done, the best practice is to convert it into DataModelContent instead.
See:
Hi, I appreciate the reply but I’m confused. I was asking if we could get a way to modify the quality that an Emesh is rendered at visually. Also I can’t bake anything to static for this. I need the emesh so I can resize quads and scroll flipbooks.
The issue I’m having has to do with the emesh forcibly using a precise rendering pipeline when on high graphics. For my use case of drawing particles with the Emesh this is undesirable. High fidelity transparent emesh particles put significantly more strain on overdraw than a standard Roblox Particle. I was hoping that we could have a property on the emesh to have it render like a particle would and use shadowmap lighting.
And sorry I should have been more clear on what my use case was. I was trying to use the emesh to render custom fluid simulated particles for my game.
Thanks for raising this. We are currently investigating this and hope to be able to identify the issue soon. In the meantime, would it be possible for you to give some more detail on what kind of motion you are applying to the MeshPart? Additionally, you’re encountering the lag with only a single MeshPart, right?
moving the part with the normal cframe property is more laggy than usually but not too bad, and trying to move the part from the bones transform property makes it allot more laggy. Also the more geometry the mesh has, the more it lags when trying to move it
in my case, i was trying to use this for a 3d particle system, which means i was moving parts around allot, so i was able to really feel that performance hit
im not the best at explaining things, but i can make a rep file if you want



