@Sneerburn it is supposed to work in the shadowmap setting also , but I tried both that AND the future setting (which I am anticipating) but the result is always as above
This is not correct. Local lights will only affect SurfaceAppearance in “Future” lighting mode. You also need to make sure your studio graphics mode is set to either Vulkan, DX11, or Metal and that your graphics quality (“Quality Level” and “Edit Quality Level”) is set high enough. Both these settings are found in File > Settings > Rendering.
From the look of your screenshots, I can tell your graphics settings are too low, as buildings are not casting shadows.
Do you mind sending a place file of this? I’d like to check the streets and the lighting settings.
I have a very simple question, maybe because everyone here is quite wanting the same… Do you know when you will release that ? I mean, are you near to release it or you still have a very long way to finish that ?
I made a small tutorial on this topic.
It looks like the SurfaceAppearance instance and it’s API are mostly figured out, but I have some feedback related to the API’s scalability with respect to detailed worlds and characters.
With the extra SurfaceAppearance instance, developers are effecively missing out on creating about +50% more PBR MeshParts before a lag spike in some cases (or +35% more Parts with PBR SpecialMeshes). Here are the performance results (test details are at the end of my post):
The SurfaceAppearance API looks convenient and simple, but there’s so much more to consider when adding features like this that shape the platform’s future. I plan to support my family through this platform for years to come, and it’s extremely important to me that features are designed with long-term goals in mind.
A lightweight API means we can do more, even if by a small margin. Here’s what we need to be able to achieve from a performance perspective:
- Low end devices need to efficiently load and run medium worlds with a handful of characters.
- Medium and high-end devices need to efficiently load and run huge PBR worlds with many PBR characters.
- Developers using Studio need to be able to view, edit, and create massive PBR worlds without lagging or running low on memory.
Here’s what we need to achieve from a features perspective:
- Color map recoloring. (Especially important for inclusive character skin tones.)
- Order-able layering of transparent PBR textures.
Here’s how SurfaceAppearance would hold developers back:
- Creating the extra instance is expensive.
- Setting each property individually crosses the “reflection layer” 4 times, which is expensive.
- The SurfaceAppearance instance is an extra child that needs to be processed in the DataModel.
- Additional child instances need to iterate over every one of their parents to fire DescendantAdded and DescendantRemoving events. This can get much slower with very long ancestry chains.
- The SurfaceAppearance instance has 4 individual image content string properties (ColorMap, MetalnessMap, NormalMap, and RoughnessMap) that need to be processed. This can add up for detailed worlds and characters.
- With
SurfaceAppearance.ColorMap
,MeshPart.TextureID
andSpecialMesh.TextureId
become redundant.
Here are 3 API alternatives that can achieve PBR without SurfaceAppearance instances:
Add PBR properties to MeshPart/SpecialMesh
MetalnessMap, NormalMap, and RoughnessMap could instead be added directly to MeshPart and SpecialMesh.
- Faster than creating an entirely new instance.
- No new instance type.
- TextureId property wouldn’t be redundant.
- These properties could bloat instances that don’t need PBR.
- Because MeshPart and SpecialMesh are so different, it would likely be more difficult for engineering to maintain identical PBR properties across different instance types.
Add a new data type for image content
A single data type would encapsulate ColorMap, MetalnessMap, NormalMap, and RoughnessMap.
The API naming would need to be well though out, but here’s are a few ideas:
-- TextureMap.new(colorMap, metalnessMap, normalMap, roughnessMap)
MeshPart.TextureMap = TextureMap.new("rbxassetid://5552747292", "rbxassetid://5552747292", "rbxassetid://5552747292", "rbxassetid://5552747292")
MeshPart.TextureMap = TextureMap.Layer({textureMap1, textureMap2})
- Extremely fast assignment (only crosses the reflection layer once.)
- No new instance type.
- Could potentially apply complex image/color transformations and compositing using the data type.
- Content string processing could be done once when you create a value of this type, instead of each time you assign each string content property.
- TextureId could be redundant, or if it could read/modify this new data type internally.
- A new value of this data type would need to be allocated in Lua. (Likely insignificant compared to instance creation/assignment.)
- Typing this out in code would be tedious.
Add string formatting to the TextureId property
I’m including this suggestion because of its similarity to the RichText beta that’s being worked on. I didn’t initially like the idea of using verbose human-readable strings for something like this, but it would make powerful use of an already existing property and still be easy to use.
The format itself would need to be well thought out and have an intuitive interface in the properties pane, but here’s a verbose human-readable example similar to rich text:
MeshPart.TextureID = "<color>rbxassetid://5552747292</><metalness>rbxassetid://5552747292</><normal>rbxassetid://5552747292</><roughness>rbxassetid://5552747292</>"
- Extremely fast assignment (only crosses the reflection layer once.)
- No new instance or data type.
- Really powerful backwards-compatible format.
- Could potentially apply complex image/color transformations and compositing using the format.
- TextureId property is repurposed.
- Identical strings reuse memory internally and are really fast to create compared to instance creation/assignment. Would likely be faster than a new data type due to internalization, even with a verbose format.
- Could benefit from utilizing the Lua string hash internally for fast access and assignment.
- Generally bad practice to expose arbitrary string formats like this.
Performance test details
- The tests reshuffle and the results are calculated over time.
- It creates nested folders in workspace to simulate a real use-case.
- More folder nesting results in worse performance, likely because AncestryChanged needs to be fired for each parent folder.
- Parts are created, added to the folder, then destroyed.
- The tests are biased towards parts that are created locally instead of streamed from the server.
- Test results will be affected by plugins that connect to workspace.DescendantAdded or workspace.DescendantRemoving. (No built-in plugin should ever connect to events like that IMO)
Here’s the source:
Performance code
-- You need to add a MeshPart named "_MeshPartExample" to the workspace for the module to clone.
-- The SurfaceAppearance Studio Beta must also be enabled.
local meshId = "rbxassetid://5552755588"
local textureId = "rbxassetid://5552747292"
-- My MeshPart configuration:
-- MeshId = "rbxassetid://5552755588"
-- RenderFidelity = Enum.RenderFidelity.Precise
-- CollisionFidelity = Enum.CollisionFidelity.Box
local partColor = Color3.new(1, 1, 1)
local partSize = Vector3.new(2, 2, 2)
local partMaterial = Enum.Material.Slate
local meshPartExample = workspace:FindFirstChild("_MeshPartExample"):Clone()
meshPartExample.Anchored = true
meshPartExample.CanCollide = false
meshPartExample.Name = ""
meshPartExample.TextureID = ""
meshPartExample.Material = partMaterial
meshPartExample.Size = partSize
-- Here we create a hierarchy of folders to simulate a real use-case.
-- A high parentDepth results in worse performance because 'DescendantAdded' needs to be fired for each ancestor.
local parentDepth = 4
local parentFolderRoot = Instance.new("Folder")
local parentFolder = parentFolderRoot
for i = 1, parentDepth do
local m = Instance.new("Folder")
m.Parent = parentFolder
parentFolder = m
end
-- Adding to workspace gives us more-meaningful results, but plugins may affect performance when run in studio.
parentFolderRoot.Parent = workspace
local parentFolder = parentFolder
local testList = {
{"control", function(loopCount)
local t0 = os.clock()
for i = 1, loopCount do
local v = i % 256
end
return os.clock() - t0
end},
{"new table", function(loopCount)
local t0 = os.clock()
for i = 1, loopCount do
-- This could get compiled-away because it has no side-effects
local v = {i % 256}
end
return os.clock() - t0
end},
{"new function", function(loopCount)
local t0 = os.clock()
for i = 1, loopCount do
-- This could get compiled-away because it has no side-effects
local v = i % 256
local f = function()
return v
end
end
return os.clock() - t0
end},
{"new Part", function(loopCount)
local t0 = os.clock()
for i = 1, loopCount do
local part = Instance.new("Part")
part.Anchored = true
part.CanCollide = false
part.TopSurface = Enum.SurfaceType.Smooth
part.BottomSurface = Enum.SurfaceType.Smooth
part.Material = partMaterial
part.Color = partColor
part.Size = partSize
part.CFrame = CFrame.new(i % 256, 0, 0)
part.Parent = parentFolder
part:Destroy()
end
return os.clock() - t0
end},
{"new WedgePart", function(loopCount)
local t0 = os.clock()
for i = 1, loopCount do
local part = Instance.new("WedgePart")
part.Anchored = true
part.CanCollide = false
part.BottomSurface = Enum.SurfaceType.Smooth
part.Material = partMaterial
part.Color = partColor
part.Size = partSize
part.CFrame = CFrame.new(i % 256, 0, 0)
part.Parent = parentFolder
part:Destroy()
end
return os.clock() - t0
end},
{"new MeshPart(TextureID)", function(loopCount)
local t0 = os.clock()
for i = 1, loopCount do
local part = meshPartExample:Clone()
part.TextureID = textureId
part.Size = partSize
part.CFrame = CFrame.new(i % 256, 0, 0)
part.Parent = parentFolder
part:Destroy()
end
return os.clock() - t0
end},
{"new MeshPart + SurfaceAppearance(ColorMap)", function(loopCount)
local t0 = os.clock()
for i = 1, loopCount do
local part = meshPartExample:Clone()
part.Size = partSize
part.CFrame = CFrame.new(i % 256, 0, 0)
local sa = Instance.new("SurfaceAppearance")
sa.ColorMap = textureId
sa.Parent = part
part.Parent = parentFolder
part:Destroy()
end
return os.clock() - t0
end},
{"new MeshPart + SurfaceAppearance(ColorMap, MetalnessMap, NormalMap, RoughnessMap)", function(loopCount)
local t0 = os.clock()
for i = 1, loopCount do
local part = meshPartExample:Clone()
part.Size = partSize
part.CFrame = CFrame.new(i % 256, 0, 0)
local sa = Instance.new("SurfaceAppearance")
sa.ColorMap = textureId
sa.MetalnessMap = textureId
sa.NormalMap = textureId
sa.RoughnessMap = textureId
sa.Parent = part
part.Parent = parentFolder
part:Destroy()
end
return os.clock() - t0
end},
{"new Part + SpecialMesh(TextureId)", function(loopCount)
local t0 = os.clock()
for i = 1, loopCount do
local part = Instance.new("Part")
part.Anchored = true
part.CanCollide = false
part.TopSurface = Enum.SurfaceType.Smooth
part.BottomSurface = Enum.SurfaceType.Smooth
part.Size = partSize
part.CFrame = CFrame.new(i % 256, 0, 0)
local mesh = Instance.new("SpecialMesh")
mesh.MeshType = Enum.MeshType.FileMesh
mesh.MeshId = meshId
mesh.TextureId = textureId
mesh.Scale = partSize
mesh.Parent = part
part.Parent = parentFolder
part:Destroy()
end
return os.clock() - t0
end},
{"new Part + SpecialMesh + SurfaceAppearance(ColorMap)", function(loopCount)
local t0 = os.clock()
for i = 1, loopCount do
local part = Instance.new("Part")
part.Anchored = true
part.CanCollide = false
part.TopSurface = Enum.SurfaceType.Smooth
part.BottomSurface = Enum.SurfaceType.Smooth
part.Size = partSize
part.CFrame = CFrame.new(i % 256, 0, 0)
local mesh = Instance.new("SpecialMesh")
mesh.MeshType = Enum.MeshType.FileMesh
mesh.MeshId = meshId
mesh.Scale = partSize
mesh.Parent = part
local sa = Instance.new("SurfaceAppearance")
sa.ColorMap = textureId
sa.Parent = mesh
part.Parent = parentFolder
part:Destroy()
end
return os.clock() - t0
end},
{"new Part + SpecialMesh + SurfaceAppearance(ColorMap, MetalnessMap, NormalMap, RoughnessMap)", function(loopCount)
local t0 = os.clock()
for i = 1, loopCount do
local part = Instance.new("Part")
part.Anchored = true
part.CanCollide = false
part.TopSurface = Enum.SurfaceType.Smooth
part.BottomSurface = Enum.SurfaceType.Smooth
part.Size = partSize
part.CFrame = CFrame.new(i % 256, 0, 0)
local mesh = Instance.new("SpecialMesh")
mesh.MeshType = Enum.MeshType.FileMesh
mesh.MeshId = meshId
mesh.Scale = partSize
mesh.Parent = part
local sa = Instance.new("SurfaceAppearance")
sa.ColorMap = textureId
sa.MetalnessMap = textureId
sa.NormalMap = textureId
sa.RoughnessMap = textureId
sa.Parent = mesh
part.Parent = parentFolder
part:Destroy()
end
return os.clock() - t0
end}
}
local testListLen = #testList
local performanceDelayList = table.create(testListLen, 0)
local loopCount = 64 -- Number of loops for each test
local iterationCount = 2048 -- Number of times to shuffle and run the tests
local iterationPrintPeriod = 16
local performanceDelayTotal = 0
local rng = Random.new()
for iteration = 1, iterationCount do
game:GetService("RunService").Heartbeat:Wait() -- Next frame
-- Randomize test order
local order = table.create(testListLen)
for i = 1, testListLen do
order[i] = i
end
for i = 1, testListLen do -- Do a simple shuffle
local j = rng:NextInteger(1, i)
order[j], order[i] = order[i], order[j]
end
-- Run tests
for _,i in ipairs(order) do
local perf = testList[i][2](loopCount)
performanceDelayTotal += perf
performanceDelayList[i] += perf
end
-- Print results
if iteration % iterationPrintPeriod == 1 or iteration == iterationCount then
local totalLoops = iteration * loopCount
print(string.format("\nIteration: %i/%i; Total = %i / %.3f s", iteration, iterationCount, totalLoops, performanceDelayTotal))
for i = 1, testListLen do
local perf = performanceDelayList[i]
print(string.format("\n%s\n\t%.2f / ms (%.2f ns)", testList[i][1], totalLoops / perf / 1000, perf / totalLoops * 1e9))
end
end
end
-- Clean up
meshPartExample:Destroy()
parentFolderRoot:Destroy()
Very cool! I finally figured out how to use this to its best, using 4k textures and the new future lighting and this is the end result!
Question, is there a date for the Surface Apperance release (going live on client)
Here’s a topic about release dates:
I may have not read this correctly, but are the default materials updated or going to be updated? (Stone, brick, grass, etc)
I would hope not, or if they did, there be an option to use the current ones, as there wouldn’t really be any point in changing them considering people will be able to upload their own with SurfaceAppearance, and doing so would completely change the appearance of many games, for better or worse (largely worse).
Thanks, but what could I plug in to the properties? Could I put a specular map in the roughness area?
Hey,
so one question about normal maps.
Rounding and bevel normals doesnt look nice on roblox
On roblox the corner looks way too much, on substance its smoothed or beveled like it made it.
its there any fix for this? they both use the same normal map.
By the way on roblox it looks a lot more saturated, and i set the environment lightings to 0.0.0 on studio.
try inverting the green channel
And use a roughness map combined with the normal map for best results
I’ve tried it out in game and it look AWESOME. I can’t wait to try it out myself.
Are textures going to be deprecated after this update?
We expect this to be available in games on the order of a couple weeks. There are still some minor bugs we’re investigating.
I have been having issues with Surface Appearances lately, the surface appearances i have on a mesh parts will often loose properties such as roughness and reflectivity. Most of the time the surface appearances will just turn into the part’s base color despite having color, metalness, normal and roughness maps.
If this is a known bug or i need to change settings in the render tab feel free to let me know.