How to improve Texture2D mesh images baked from a RenderTexture using CommandBuffer DrawMesh

I’m hoping to get some insight on a few problems on a niche topic.

I am attempting to generate Texture2D images of meshes on the fly during runtime. This is mainly for turning GameObjects into inventory items without having to explicitly implement each one on the dev side. I am having some success but have run into some strange issues I don’t know how to approach.

First, the in-editor result:


Decent results, however you can see the handaxe in the bottom-left has a weird mirroring issue (#1) despite it appearing to have the same setup as other items. Additionally, there are no shadows, which I probably must accept (#2).

This is the in-build result:


Only the outlines of the meshes are drawn (#3). If you look closely you can see there is indeed a tad bit of color (if that’s significant).

Questions (code below):
issue #1: How can this weird mirroring possibly happen? The matrices are the same per operation, and submeshes are each drawn once. I am perplexed.
issue #2: Can you ‘add’ light to CommandBuffer operations? Do I maybe have a setup ‘backwards’ (i.e. Light.AddCommandBuffer)
issue #3: Why is this build-specific? Is it depth-bound? Could it be Material-related (even though some color shows?)

If these were your issues, what would you poke at first? Maybe I’m simply trying to achieve this with the wrong approach. How would you implement this functionality? Or maybe I’m trying too hard to be lazy and should create each image by hand (ew please no).

Here is my code:


/**
 * Create a Texture2D image of the given item to be used in the UI.
 * Locks *just* in case of active RenderTexture contention.
 */
public static IEnumerator BakeInventoryImage(ContainableBody item, Action<Texture2D> callback)
{
    Vector2 size = GetPositionalDeltas(item.gridSize.x, item.gridSize.y); // tile count
    Vector2Int sizeInt = new Vector2Int(Mathf.RoundToInt(size.x), Mathf.RoundToInt(size.y)); // size in pixels

    RenderTexture renderTexture = RenderTexture.GetTemporary(sizeInt.x, sizeInt.y, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default, 8);
    Debug.Assert(renderTexture.isReadable, instance.name + ": renderTexture.isReadable=false");

    // Make target texture image
    Texture2D image = new Texture2D(sizeInt.x, sizeInt.y, TextureFormat.ARGB32, false);
    Debug.Assert(image.isReadable, instance.name + ": Texture2D.isReadable=false");
    image.name = item.name + ".Baked";
    image.wrapMode = TextureWrapMode.Clamp;

    yield return new WaitForEndOfFrame();
    DrawMesh(item, renderTexture);

    lock (instance)
    {
        RenderTexture.active = renderTexture;

        image.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0);
        image.Apply();

        // Restore main active render texture
        RenderTexture.active = null;
    }

    RenderTexture.ReleaseTemporary(renderTexture);

    callback(image);
}

/**
 * Draw the given item onto the given renderTexture using a commandBuffer.
 * Scale the mesh and handle rotation by using the renderer's transform.
 * 
 * Note that a call to ExecuteCommandBuffer may(?) require waiting for end of frame beforehand.
 */
private static void DrawMesh(ContainableBody item, RenderTexture renderTexture)
{
    Transform rendererTransform = item.renderer.transform;
    Vector3 rendererScale = rendererTransform.lossyScale;
    Mesh mesh = item.meshFilter.sharedMesh;
    Bounds meshBounds = mesh.bounds;

    List<Material> materials = new List<Material>();
    item.renderer.GetSharedMaterials(materials);

    float viewBoxRadius = 25f;
    Vector3 pivotedCenterDiff = Vector3.Scale(rendererScale, meshBounds.center - (Vector3)((Vector2)(item.meshRotation * meshBounds.center)));
    meshBounds.size = new Vector3(item.gridSize.x, item.gridSize.y) * MenuManager.ItemBorderScale / MenuManager.InventoryCellScale;

    Matrix4x4 meshMatrix = Matrix4x4.TRS(pivotedCenterDiff, item.meshRotation, rendererScale);
    Matrix4x4 lookMatrix = Matrix4x4.TRS(new Vector3(0, 0, -viewBoxRadius), Quaternion.identity, Vector3.one);
    Matrix4x4 orthoMatrix = Matrix4x4.Ortho(meshBounds.min.x, meshBounds.max.x, meshBounds.min.y, meshBounds.max.y, -viewBoxRadius, 2f * viewBoxRadius);

    // Create a CommandBuffer 'canvas' to draw the mesh onto
    CommandBuffer commandBuffer = new CommandBuffer();
    commandBuffer.name = item.name + " Item Drawer";

    commandBuffer.SetRenderTarget(renderTexture);
    commandBuffer.SetViewProjectionMatrices(lookMatrix, orthoMatrix);
    commandBuffer.ClearRenderTarget(true, true, Color.Lerp(Color.clear, Color.white, .5f));

    // draw each submesh
    for (int i = 0; i < mesh.subMeshCount; i++)
    {
        commandBuffer.DrawMesh(mesh, meshMatrix, materials[i], i);
    }

    Graphics.ExecuteCommandBuffer(commandBuffer);
}

Are bumps allowed…?

Hmm those outlines look strangely like normal maps around the edges. What exactly is generating them and what is their purpose?

In the build version, perhaps some shader stripping is going on. You could try adding the appropriate shaders to the list to always include just to test if that is part of the problem. Although, lately I’ve seen issues like that pop up a lot here and I’ve even had them myself a few times where even adding them to the list didn’t help. The only way to ensure they worked was to add them to some visible object in the very first scene loaded at startup.

For the lighting, the way I do this is to just hard code one or more lights directly into a custom shader. It literally can just be a color and a vector that I use in the shader as though it were a directional light. Makes it easy to test out via the inspector while not having to deal with all the nonesense of actually setting up UI-only lights and then push that to the rendering system somehow.

1 Like

Thank you so much for your input and wisdom!

These meshes are using materials that use the URP/Lit shader. Adding it to the “Always Included Shaders” list make the build compilation an ETA of around 20 hours O_O. Especially with the chance it may not even fix it, I may have to postpone trying this solution for around a week till I get a better computer, or I may proceed with creating a new shader to additionally handle that lighting trick you mentioned. I will keep you posted.

Yeah, that’s to be expected. Generally speaking it’s worth avoiding using the ‘always include’ option since it will compile every possible variation of the shader. Even with a beefy computer you could be looking at several hours, at least for the first build. Usually they get faster after that due to caching as long as you don’t change the shaders.

In this case however I think the best option would be to just create your own simple shader. You don’t need any of the features that the default lit one provides and it will let you supply your own lighting calculation which in this case would be exceptionally simple and easy to implement without having to deal with any lights in the scene. Who knows, it might even solve that weird mirroring issue you saw.

1 Like

Thank you again! I confirmed that explicit additions to the “Always Included Shaders” list does result in proper rendering. You have made my month :slight_smile: