[Studio Beta] Quad Support for EditableMesh APIs

already possible, i made this with ai 2 months ago in like 3 hours

10 Likes


this editable mesh stuff made me wish the API wasnt useless since its inaccessible

4 Likes

Def haha,

I am kinda mad rn tho :sob:

I have to recreate the whole topology system. Im hoping this will help.

These beta features can be used in real games, right?

2 Likes

nope! only in studio, once they get released you can put them in game

4 Likes

Yes, but your current(old) version does not support dynamic triangles.

New “Quad Support” does.

1 Like

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. :joy: :folded_hands: :sob:

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

3 Likes

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)
2 Likes

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)

3 Likes

Cool, Noted. More better buildings.

3 Likes

Are there any future plans to support an arbitrary number of vertices per face?

1 Like

When is this feature expected to go out of Beta?

Blender in Roblox is all we wanted!

1 Like

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