[Solved] DX10 - Problem with MipLODBias & Moire effect

Started by
10 comments, last by Trigle 12 years, 10 months ago
Hi all,

I am new to this forum and I think I'll start off with a relatively tame post; I tend to try to solve all my own bugs and it's frustrating to have to ask about it but I've tried almost everything I can think of to resolve the problem.
It may be a misconception of mine or maybe it's a common issue, I don't know, I don't know many 3D graphics peeps.

Anyway on to the issue at hand:

I have a manually generated terrain mesh and my terrain is rather large. I've upped my far plane to some number like 10000 so I can view all of my terrain without having to move around to check if its all rendered correctly, that's no problem. However I have noticed that when sampling from a texture with a MipLODBias of 0 using a linear sampler, I have a moire effect at far distances. If I provide a negative LOD bias I can avoid this but I get a shimmering effect from mesh poly's closer to the camera. At first I thought that it may be that my texture is not big enough so I tried a bigger texture, no joy.

I am using a texture atlas of 512x512 pixels; which means each of the 4 images is 128x128 [Edit: should be 256x256] pixels in size. The negative LOD bias fixes this temporarily but I am aware that the control panels of drivers allow the ability to 'clamp' this value, I'm not sure what it would be clamped to and it still may solve the problem, but I am curious to whether there is another solution for this? or is my far plane simply too far?


Any help would be appreciated :) thank you rolleyes.gif

P.S. I am not using the Effect framework and not using DXUT.
Advertisement
Ok it's definitely something to do with the linear sampling and mip-mapping, I have read a bit more about this, apparently because I'm using a texture atlas, the changes between the texcoords are changing so rapidly that it confuses the mip-map level selection. Although I don't entirely understand this, I have discovered the function SampleGrad and SampleLevel (I'm using shader model 4).

I can't find many relevant examples of SampleGrad out there, can anyone explain to me how I could implement this to improve my sampling at far distances?

Example:

dx10problem.jpg

Maybe I am missing something fundamental to using texture atlas approaches, I have already added and subtracted an offset of 0.01f to either side of the sub-texture to remove any borders at mip-map level 0 but as it gets further away the sampling is causing obvious issues. It wasn't as bad in my DX9 implementation but in my DX10 it is much more visible. I'm really at my wits end with this one :)

I am using a texture atlas of 512x512 pixels; which means each of the 4 images is 128x128 pixels in size.
Does this mean that each sub-image has a 64px wide border around it? If so, what's the 0.01f offset trick do?

Ok it's definitely something to do with the linear sampling and mip-mapping, I have read a bit more about this, apparently because I'm using a texture atlas, the changes between the texcoords are changing so rapidly that it confuses the mip-map level selection. Although I don't entirely understand this, I have discovered the function SampleGrad and SampleLevel (I'm using shader model 4).[/quote]Yeah, when you sample a texture, the hardware measures the rate-of-change of the tex-coords, and uses that info to select a mip level. When you do your manual texture-wrapping logic, you're having a big effect on the rate-of-change at the edges.
It would help if you posted up some of your shader code, but I'll just pretend this is your code ;)float2 uv = input.uv;
uv = /*do wrapping / atlas logic*/
float4 colour = tex.Sample( S, uv )

What the driver does with that code, is basically turn it into this:float2 uv = input.uv;
uv = /*do wrapping / atlas logic*/
float2 ddx_uv = ddx(uv);
float2 ddy_uv = ddy(uv);
float4 colour = tex.SampleGrad( S, uv, ddx_uv, ddy_uv );
The problem is that it's done it's rate of change calculations after you've done your wrapping logic.

You can fix it by doing the rate-of-change calculations yourself at the appropriate stage:float2 uv = input.uv;
float2 ddx_uv = ddx(uv);
float2 ddy_uv = ddy(uv);
uv = /*do wrapping / atlas logic*/
float4 colour = tex.SampleGrad( S, uv, ddx_uv, ddy_uv );
Thanks Hodgman I knew you'd be good for it :)
[color="#1C2837"]Does this mean that each sub-image has a 64px wide border around it? If so, what's the 0.01f offset trick do?[/quote]

Sorry this should have been 256x256 (sorry I've been slow since I got out of hospital mate ;))

The 0.01f offset is added to the start or deducted from the end of every single generated tex-coord to simulate a border, it also should mean that sampling done by linear samplers do not pick pixels outside of the texture, sure I should put in a border of the same value to prevent seam issues; note taken :)

So that would be 0.01f x 256, which is 2.56 pixels for each border, giving me a 3 pixel border for each texture yeah?

[color="#1C2837"]It would help if you posted up some of your shader code, but I'll just pretend this is your code ;)[/quote]

You see I'm generating the tex-coords when I generate the mesh; it's not how I want to do it - I have always considered this an unnecessary approach to texturing as all of those 'duplicate' tex-coords are being stored against each vertex which is a massive waste of memory.
I will be changing this but haven't resolved an alternative approach to doing the texturing within the vertex shader (determining the tex-coords from the height on each and every vertex from the y component of it's untransformed position) and sampling in the pixel shader.


[color="#1C2837"]You can fix it by doing the rate-of-change calculations yourself at the appropriate stage:[color="#1C2837"][color="#000000"]float2 uv [color="#666600"]=[color="#000000"] input[color="#666600"].[color="#000000"]uv[color="#666600"];[color="#000000"]
float2 ddx_uv [color="#666600"]=[color="#000000"] ddx[color="#666600"]([color="#000000"]uv[color="#666600"]);[color="#000000"]
float2 ddy_uv [color="#666600"]=[color="#000000"] ddy[color="#666600"]([color="#000000"]uv[color="#666600"]);[color="#000000"]
uv [color="#666600"]= [color="#880000"]/*do wrapping / atlas logic*/[color="#000000"]float4 colour = tex.SampleGrad( S, uv, ddx_uv, ddy_uv );

[/quote]

What is the default input tex-coords that you've provided each vertex, assuming ddx and ddy need valid values in order to make a difference?

My idea to solve this would be to allocate each vertex a 'texture reference and a triangle reference' in which the shader could instantly know which area of the texture atlas it needed to be sampled from. Each reference could be extracted using bitwise ops from a 32-bit integer as 16-bit integers saving myself space per vertex over the traditionally used for storing the UV coords. This could then be looked up in an array of values to get the corresponding tex-coords, unless there's an easier way for that.

I have only started my DX10 version this week so my shader is very basic.

Vertex Shader
cbuffer main
{
matrix WorldViewProj;
};
struct VS_INPUT
{
float3 position : POSITION;
float3 normal : NORMAL;
float2 texcoord : TEXCOORD0;
};
struct VS_OUTPUT
{
float4 position : SV_POSITION;
float2 texcoord : TEXCOORD0;
};
VS_OUTPUT vsMain(in VS_INPUT vIn)
{
VS_OUTPUT vOut = (VS_OUTPUT)0;
vOut.position = mul(float4(vIn.position, 1.0f), WorldViewProj);
vOut.texcoord = vIn.texcoord;
return vOut;
}


Pixel Shader
sampler s0 : register(s0);
cbuffer main
{
texture2D groundTex;
};
struct VS_OUTPUT
{
float4 position : SV_POSITION;
float2 texcoord : TEXCOORD0;
};
float4 psMain(in VS_OUTPUT vOut) : SV_TARGET
{
vOut.color = groundTex.Sample(s0, vOut.texcoord);
return vOut.color;
}



I am using shader model 4.

The 0.01f offset is added to the start or deducted from the end of every single generated tex-coord to simulate a border, it also should mean that sampling done by linear samplers do not pick pixels outside of the texture
So that would be 0.01f x 256, which is 2.56 pixels for each border, giving me a 3 pixel border for each texture yeah?
That's a ~3px border at mip 0, a ~1px border at mip 1, and a ~0px border for all other mips.
This is definately your problem -- after you've gone a few steps down the mip-chain, your border is no longer big enough, and the bilinear filter starts bleeding colours from neighbouring tiles.

You see I'm generating the tex-coords when I generate the mesh; it's not how I want to do it - I have always considered this an unnecessary approach to texturing as all of those 'duplicate' tex-coords are being stored against each vertex which is a massive waste of memory. [/quote]Does this mean you're breaking a flat surface up into lots and lots of polygons?
Yeah, that's sub-optimal these days, but it does mean that your only problem is the fact that your border is too small. All that ddx/ddy/SampleGrad stuff doesn't matter if your tex-coords have been pre-generated like this.


The larger you make your borders, the further down the mip-chain you can go before you start getting bleeding issues.
To be really safe, you could split your 512x512 target nto four 256x256 quadrants. Take each quadrant and put a 128x128 sized texture in the centre, with a 64px border around it.
i.e. if you r 128px texture looks like this:TL|TM|TR
ML|MM|MR
BL|BM|BR
Then your 256px version with borders looks like this:BR|BL|BM|BR|BL
TR|TL|TM|TR|TL
MR|ML|MM|MR|ML
BR|BL|BM|BR|BL
TR|TL|TM|TR|TL




If you wanted to do the tex-coord generation inside your shader, instead of doing it in advance, you can do it in your pixel shader as follows. This is assuming the above layout of a 512px atlas with 128px textures and 64px border areas.

Vertex Shader
struct VS_INPUT
{
float3 position : POSITION;
float3 normal : NORMAL;
float2 texcoord : TEXCOORD0;//regular tex-coords, as if you were using regular (non-atlas) textures
float2 atlasSector : TEXCOORD1;//either (0,0), (1,0), (0,1) or (1,1) -- which part of the atlas to use
};
struct VS_OUTPUT
{
float4 position : SV_POSITION;
float2 texcoord : TEXCOORD0;
float2 atlasSector : TEXCOORD1;
};
VS_OUTPUT vsMain(in VS_INPUT vIn)
{
VS_OUTPUT vOut = (VS_OUTPUT)0;
vOut.position = mul(float4(vIn.position, 1.0f), WorldViewProj);
vOut.texcoord = vIn.texcoord;
vOut.atlasSector = vIn.atlasSector;
return vOut;
}

Pixel Shader
float4 psMain(in VS_OUTPUT vOut) : SV_TARGET
{
float2 uv = frac(vOut.texcoord);//perform wrapping into the 0.0 to 1.0 range
uv *= 0.25;//resize from a 512x512 area to a 128x128 area
uv += float2(0.125, 0.125);//offset by the 64px border
uv += vOut.atlasSector * 0.5;//offset into the desired quadrant
vOut.color = groundTex.SampleGrad(s0, uv, ddx(vOut.texcoord*.25), ddy(vOut.texcoord*.25));
return vOut.color;
}
Wow I didn't think of the bordering in that way to be honest. I'll do as you suggest and see how it works out :)

struct VS_INPUT
{
float3 position : POSITION;
float3 normal : NORMAL;
float2 texcoord : TEXCOORD0;//regular tex-coords, as if you were using regular (non-atlas) textures
float2 atlasSector : TEXCOORD1;//either (0,0), (1,0), (0,1) or (1,1) -- which part of the atlas to use
};

[/quote]

Yeah I was thinking similar to this, thanks for the idea you've just inspired me for a cool solution.
OK I have tried this as a solution and unfortunately I'm still having this problem.

Here's the vertex and pixel shader respectively.

cbuffer main
{
matrix mWorldViewProj;
};
struct VS_INPUT
{
float3 position : POSITION;
float3 normal : NORMAL;
float2 texcoord : TEXCOORD0;
float2 atlasSector : TEXCOORD1;
};
struct VS_OUTPUT
{
float4 position : SV_POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
float2 atlasSector : TEXCOORD1;
};
VS_OUTPUT vsMain(in VS_INPUT vIn)
{
VS_OUTPUT vOut = (VS_OUTPUT)0;
vOut.position = mul(float4(vIn.position, 1.0f), mWorldViewProj);
vOut.texcoord = vIn.texcoord;
vOut.atlasSector = vIn.atlasSector;
return vOut;
}

sampler s0 : register(s0);
cbuffer main
{
texture2D groundTex;
};
struct VS_OUTPUT
{
float4 position : SV_POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
float2 atlasSector : TEXCOORD1;
};
float4 psMain(in VS_OUTPUT vOut) : SV_TARGET
{
float2 uv = frac(vOut.texcoord);//perform wrapping into the 0.0 to 1.0 range
uv *= 0.25;//resize from a 512x512 area to a 128x128 area
uv += float2(0.125, 0.125);//offset by the 64px border
uv += vOut.atlasSector * 0.5;//offset into the desired quadrant
vOut.color = groundTex.SampleGrad(s0, uv, ddx(vOut.texcoord*.25), ddy(vOut.texcoord*.25));
return vOut.color;
}



And finally the sampler description:
D3D10_SAMPLER_DESC sampDesc;
ZeroMemory(&sampDesc, sizeof(sampDesc));
sampDesc.Filter = D3D10_FILTER_MIN_MAG_MIP_LINEAR;
sampDesc.AddressU = D3D10_TEXTURE_ADDRESS_CLAMP;
sampDesc.AddressV = D3D10_TEXTURE_ADDRESS_CLAMP;
sampDesc.AddressW = D3D10_TEXTURE_ADDRESS_CLAMP;
sampDesc.ComparisonFunc = D3D10_COMPARISON_NEVER;
sampDesc.MinLOD = 0;
sampDesc.MaxLOD = D3D10_FLOAT32_MAX;


Here's the screenshot and my texture screenshot (with guides from Photoshop to show the texture position):

dx10dirty.jpg
textureatlas.jpg

The problem still persists :(
since you are using directx 10, try using a texture array and see if that helps.
Wisdom is knowing when to shut up, so try it.
--Game Development http://nolimitsdesigns.com: Reliable UDP library, Threading library, Math Library, UI Library. Take a look, its all free.

since you are using directx 10, try using a texture array and see if that helps.


Thanks for the idea but it seems that changing this value:


sampDesc.MaxLOD = 6;



in my sampler has solved the problem, it seems to have worked like this with respect to my borders (in my case at least as explained previously by hodgman):

Level 0 - 64px
Level 1 - 32px
Level 2 - 16px
Level 3 - 8px;
Level 4 - 4px
Level 5 - 2px
Level 6 - 1px

Sweet :)
The fix will be case specific. Since you are forcing the video card to use that mip level as the max. So, if you change the texture size, it will mess up your fix. Placing your textures into an array will fix your problem without having to result to a case specific fix.
Wisdom is knowing when to shut up, so try it.
--Game Development http://nolimitsdesigns.com: Reliable UDP library, Threading library, Math Library, UI Library. Take a look, its all free.

This topic is closed to new replies.

Advertisement