- Home /
How to use a shader to write to a mipmap (partial mipmap update)
Hello,
I am using a ComputeShader to write data to a RenderTexture. I need to update MipMaps after writing to the texture, as proper (updated) MipMaps are required to visualize the texture using a normal material / graphics shader.
The challenge is. The texture is very large (16k x 16k). The modification to it are small and frequent. So I need to do the job within the existing GPU texture buffers using a compute shader. Preferably I try to limit the Mipmap update to the effected region.
So my questions are:
- How to update a MipMap from a ComputeShader?
- Is there a way to directly access/write to a specific texture mipmap from a Shader? (i.e. using level, UV to index)
Many Thanks
Tobias
Answer by BastianUrbach · Feb 03 at 04:28 PM
I don't think you can pick a mipmap level in the shader. What you can do is assign a specific mipmap level to the shader variable on the C# side:
computeShader.SetTexture(kernel, name, renderTexture, mipmapLevel);
If you want to access all mipmaps in the shader, you need one RWTexture2D variable per mipmap.
Answer by TobiasWeber · Feb 03 at 05:40 PM
Thanks a lot. Yes that seems to be a promising option.
Just tricky on how to handle different texture sizes, mipmap levels. As I need to assign individual texture variables in the shader.
Follow on:
- Would it be possible to use a NativePtr to access the texture and just expect the MipMaps to be found at some index ranges behind w x h?
- Is the shader executed in sequence with respect to dimensions? Let's say, I want to use id.xy as 2D coordinates in the picture and id.z as index for the mipmap levels. Can I assume that it first iterates over id.xy before incrementing id.z to go to the next mipmap. Or do I need to invoke a separate shader call for each mipmap?
Would it be possible to use a NativePtr to access the texture and just expect the MipMaps to be found at some index ranges behind w x h?
If you use Texture2D.GetRawTextureData, you get the CPU-side texture memory as a NativeArray, which includes mipmaps. For example, for an 8x8 32-bit texture, the first 256 bytes are mipmap level 0, the next 64 are mipmap level 1, the next 16 are mipmap level 2 and so on. Don't confuse this with Texture.GetNativeTexturePtr(), that just gives you some graphics API struct (e.g. a VkImage on Vulkan), not the actual texture data. To a graphics API, a texture is just GPU memory and some CPU-side metadata and handle. Not all Unity textures have a CPU-side copy (RenderTextures do not) and GPU-side changes are not visible on the CPU side. To get the GPU side texture data, you have to explicitly read from the GPU, e.g. with Texture2D.ReadPixels.
Is the shader executed in sequence with respect to dimensions?
No, at least not really or reliably. It's kinda complicated. The point of GPU compute is that a lot of work is done in parallel. You can not expect that two compute shader threads are executed in any particular order, as they may well be executed at the same time. That said, I believe to the extent that the compute shader can not work in parallel (you usually have more threads than what can run in parallel), it is executed in the expected order. But that's difficult to make use of. So basically it may work under the right circumstances but that's deceptive as it may fail randomly, especially on different hardware. I think one dispatch per mipmap is the way to go.
Great explanation. So using setTexture is the most promising approach for my problem. I am going to create 16 textures, one for each mipmap level on a 16k tex and use a chain of 16 indirectly dispatched kernels. I need to do the job on the GPU to avoid transfer of this huge texture and don't want to block CPU for to long. + I need to limit MipMap update to the affected zones.
Just stumbled across CustomRenderTexture. Currently I am using RenderTexture (RWTexture) + ComputeShader. May I benefit from using CustomRenderTexture in my case? I see a value of using UpdateZones, as I hope it limits the automaticMipMap generation to these areas. However the information in the docs are sparse.
Does CustomRenderTexture + UpdateZone + automaticMipMap=true limit the update of the MipMaps to the affected area?
This is the first time I hear about CustomRenderTexture, I think it's a relatively new thing. Looks promising but I don't know much about it.
If you do one dispatch per mipmap then you should only need two RWTexture2D variables in the shader (read from one and write to the other) and these can point to different mipmap levels in the same texture so you don't actually need multiple textures. Basically my approach would be to use a compute shader like this:
#pragma kernel CSMain
RWTexture2D<float4> In, Out;
uint2 Offset;
[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID) {
uint2 p = (id.xy + Offset) * 2;
float4 a = In[p + uint2(0, 0)];
float4 b = In[p + uint2(0, 1)];
float4 c = In[p + uint2(1, 0)];
float4 d = In[p + uint2(1, 1)];
Out[id.xy + Offset] = (a + b + c + d) / 4;
}
And invoke it like this:
void UpdateMipmaps(RenderTexture rt, int xMin, int yMin, int xMax, int yMax) {
var groupSizeX = 8f;
var groupSizeY = 8f;
for (int i = 0; i < rt.mipmapCount - 1; i++) {
xMin = Mathf.FloorToInt(xMin / 2f);
yMin = Mathf.FloorToInt(yMin / 2f);
xMax = Mathf.CeilToInt(xMax / 2f);
yMax = Mathf.CeilToInt(yMax / 2f);
var groupsX = Mathf.CeilToInt((xMax - xMin) / groupSizeX);
var groupsY = Mathf.CeilToInt((yMax - yMin) / groupSizeY);
cs.SetInts("Offset", xMin, yMin);
cs.SetTexture(0, "In", rt, i);
cs.SetTexture(0, "Out", rt, i + 1);
cs.Dispatch(0, groupsX, groupsY, 1);
}
}
I don't know if this is the fastest way to do it though.