Allow read access to texture properties in SurfaceAppearance and MaterialVariant

Problem

As a Roblox developer, it is currently impossible to read various properties of SurfaceAppearance and MaterialVariant from a script because they are set to Plugin Security. The classes have the properties ColorMap, MetalnessMap, NormalMap, and RoughnessMap which are set to plugin security so reading these properties from scripts results in a run time error.

Use Case

My specific use case deals with the functionality of ContentProvider:PreloadAsync(). I have developed a system which dynamically generates a list of sound and image assets that are to be preloaded by the client. There’s a set amount of common assets that are loaded first which are common to all game maps. In addition to the common assets, there are additional assets that are map specific which are loaded after the common assets. Some items in both common assets and map specific assets have SurfaceAppearence classes associated with them. MaterialVariant is exclusively a common asset.

The code that generates the asset list for this system is below:

-- Builds the preload asset list.
function buildPreloadedAssetList()
	local sourceList = {
		game.ReplicatedFirst;
		game.ReplicatedStorage;
		game.StarterGui;
		game.ServerStorage:FindFirstChild("MapParts");
		game.ServerStorage:FindFirstChild("Tools");
		game.ServerStorage:FindFirstChild("ToolModels");
		game.ServerStorage:FindFirstChild("Weapons");
		--game.ServerStorage:FindFirstChild("");
	}

	-- Makes sure that all assets in the list are unique.
	local function unique(list)
		local newlist = {}
		table.sort(list)
		local prev = nil
		for _, item in pairs(list) do
			if item ~= prev then
				table.insert(newlist, item)
			end
			prev = item
		end
		return newlist
	end

	-- Generates the asset list for the given path.
	local function generateAssetList(root, list)

		-- Processes the asset URL.
		local function processURL(list, str)
			local flag = false
			if str ~= nil and str ~= "" then
				-- If the string starts with http, then we extract the
				-- asset Id and replace the prefix with rbxassetid.
				if string.find(str, "http://", 1, true) ~= nil then
					local txb = string.split(str, "=")
					local aId = txb[#txb]
					str = "rbxassetid://" .. aId
					flag = true
					-- If the string starts with rbxassetid, then we do
					-- nothing.	
				elseif string.find(str, "rbxassetid", 1, true) ~= nil then
					flag = true
				end
				-- If one of the two branches above was taken, then this
				-- is a valid asset Id.
				if flag == true then
					table.insert(list, str)
				end
			end
		end

		-- Currently, it seems that only sounds and images
		-- are supported.
		local objectList = root:GetDescendants()
		local data
		for _, item in pairs(objectList) do
			if item:IsA("Decal") then
				processURL(list, item.Texture)
			elseif item:IsA("Sound") then
				processURL(list, item.SoundId)
			elseif item:IsA("MeshPart") then
				processURL(list, item.MeshId)
				processURL(list, item.TextureID)
			elseif item:IsA("SpecialMesh") then
				processURL(list, item.MeshId)
				processURL(list, item.TextureId)
			elseif item:IsA("MaterialVariant") then		-- Generates Runtime Error
				--processURL(list, item.ColorMap)
				--processURL(list, item.MetalnessMap)
				--processURL(list, item.NormalMap)
				--processURL(list, item.RoughnessMap)
			elseif item:IsA("SurfaceAppearance") then	-- Generates Runtime Error
				--processURL(list, item.ColorMap)
				--processURL(list, item.MetalnessMap)
				--processURL(list, item.NormalMap)
				--processURL(list, item.RoughnessMap)
			elseif item:IsA("Sky") then
				processURL(list, item.MoonTextureId)
				processURL(list, item.SunTextureId)
				processURL(list, item.SkyboxBk)
				processURL(list, item.SkyboxDn)
				processURL(list, item.SkyboxFt)
				processURL(list, item.SkyboxLf)
				processURL(list, item.SkyboxRt)
				processURL(list, item.SkyboxUp)
			end
		end
	end
	
	-- Generate asset list for common items.
	for _, item in pairs(sourceList) do
		generateAssetList(item, preloadAssetList)
		preloadAssetList = unique(preloadAssetList)
	end
	preloadSkipCount = #preloadAssetList
	preloadCheck = true
end

A similar process is used to generate the map specific asset list. Within game.ReplicatedFirst, there is a ScreenGui and a LocalScript. The local script first clones the ScreenGui to PlayerGui and then sends an event to the server requesting the asset list. The server responds with the list of assets that the client should load. When the client is done loading assets, it tells the server that it’s done and waits for a response. Once the server responds, the client removes the ScreenGui from PlayerGui and exits while the server spawns the player’s character in the game.

The assets in question can be manually specified, but then what’s the point of having an automatic system? All that’s being asked for is read access to the afore mentioned properties by scripts.

Possible Solution

One possible solution is to remove the Plugin Security flag and set the properties to read only. However, if it’s by design that plugins are supposed to have full access, then an alternate solution would be to create new properties that are mirrors of the original properties with a security context that allows scripts to read those and set them to read only.

Conclusion

If Roblox is able to address this issue, it would improve my development experience because then I can include more items for the preload phase so the client doesn’t have to download them on the fly which will prevent visual artifacts in the game while those assets are downloaded.

6 Likes

This would benefit me and a lot of other people that use a loading screen in their game!

Should be processURL(list, item.TextureId), right?

1 Like

Not that one. What I have is correct.

2 Likes

In practice, it does work. No failures are registered when doing it that way. Before, meshes wouldn’t load either. Now they do. So they did improve it without telling anyone but haven’t updated the documentation yet.

EDIT:

Here’s proof of what I’m saying:

@xyrafrost Interesting, when I first wrote this code, you could only pass asset Ids to it, which I’m still doing. It might be worth the effort to include the new functionality.

1 Like

You can’t directly preload SurfaceAppearance instances (so you most likely can’t preload MaterialVariants either)

Quote from a Roblox staff:

(I also just tested preloading a SurfaceApperance myself and it didn’t work.)

1 Like

It appears that someone changed the documentation for SurfaceAppearance but the change didn’t make it into the engine. You still can’t read those four properties because it errors out with a Plugin security error.