[Solved] Drawing a sprite inside a 3D-Model

Hello guys^^

I have
worked with Monogame for a little while now and have a basic understanding of the
graphical processes. Normally, if I have a problem, I am able to google my way
to as satisfying solution, thanks to forums like this :wink: But now there is a situation
where I can’t seem to find a way to generate the desired graphical result.

Basically, what I want to achieve is to draw the back and front-halves of a 3D-Model separately and between those I want to be able to draw other Models and 2D-Sprites.

I am already able to draw the back and front-halves of Models separately by modifying
the near/far-plane of the corresponding orthographic projection-matrix.

Now, I want to put a second Model and then a sprite in between those two halves.

This should be easy, I just have to make the corresponding draw-calls in the right order.
To draw the Models I basically call:

foreach (ModelMesh mesh in Model.Meshes)
mesh.Draw();

and to draw the sprite I just begin (and then end) the spriteBatch.

However, the Modell in between is suddenly drawn with the triangles in the wrong order.

This is usually cured by setting:

graphicsDevice.DepthStencilState = DepthStencilState.Default;

before the Model is drawn, but it causes another problem in which both halves of the first
Model are drawn in front of the second Model and sprite. But the back-halve should be behind…

If you are wondering what you are seeing in those images: the second picture shows the 3d-Model of a tunnelled through planet and a sprite of the atmosphere of the planet. The first picture shows the Model of a ring-like space station that rotates around the planet. In order for the Planet and atmosphere -sprite to be drawn inside the ring, the ring has to be separated in back and front halves which must be drawn separately.

I experimented a lot with the DepthStencilState in order to achieve the desired
effect but there is always one or multiple flaws…

I guess the easiest way to solve the problem would be to be able to instantly render
the contents of the z-buffer to the screen and then clear the z-buffer an
repeat the process for each (halve-)Model.

But I read this is not so easy and opens another can of worms.

Hello Andreas,

By adjusting far/nearplane halfway during draw,
the values written to the depth buffer have no colleration from model to model.

You could render the sprite with the depth buffer enabled at the right depth, so it’s drawn in front of the planet and behind the ring (where it matters). If you don’t have a depth that works (i.e. the sprite intersects with the ring when it’s in front of the planet), you could render the sprite with a depth map or depth offset hardcoded in the shader so the center of the sprite is rendered closer to the camera. A radial gradient like the following would do the trick (make sure you scale the depth offset, probably easiest with a shader parameter; maybe make it interactive to find the right scale).

2 Likes

A quick hack would be to draw the far half of the planetary ring first with the depth buffer disabled.
bit you really shouldn’t change near/far plane between draws.

If the planet model (sphere) has normals, you could render the planet and the atmosphere using a pixel shader with fresnel.

Another idea,
have a sphere with a texture that has a white ring around it and the rest fades to transparent. and draw that on top of the planet.
on every draw you set that sphere to have the same orientation as the camera, so that the white ring is drawn at the visible edges.

I might have misunderstood the desired result, but is there any reason why not to render sprite on polygon that rotates towards camera (billboard)? Sprites are ultimately rendered on polygons, in this case it would be just another mesh. That would mean no issue with sorting, etc.

Hello again,

thank you for the quick replies. I have experimented with your suggestions and that brought me to (what I think is) a very simple solution:

Since I am using an Orthographic projection there is no harm in flattening the planet (but not the ring) via a z-scaling matrix.
Then I should be able to draw all other sprites before and after (e.g. atmosphere) the planet just by using the layerDepth parameter in the spriteBatch.Draw-function.

The ring can be drawn once like a normal Model, without the far/nearplane modification.

I will try to implement this Idea as soon as possible and keep you updated.
But please, if you have any thoughts on this approach, let me know.

Best regards.^^

1 Like

I don’t get it fully to be honest.
The only problem i would see with doing this is the area were the sphere overlaps with the pipe area. I would probably merge the model meshes as a first choice and cut the vertices out of the areas of the sphere as needed. As a second i would use a rendertarget and some polygon covering for that pipe top just to get a depth stencil draw. Were the pipe overlaps the sphere data as stencil depth. Then draw the pipe then the sphere with that stencil texture and in normal order (pipe only draws were the stencil is then sphere draws except were the stencil is) with the depth buffer on. Then the atmosphere sprite. I can only see the solution as stenciling out the surface of the sphere were the pipe makes contact, but maybe you guys see something i don’t.

Since your not asking about that ill just say for drawing the back of a model there is two easy ways.

You can draw the back of a model by just flipping the winding on the graphics device via the rasterizer state.

public RasterizerState rs_cull_ccw_solid = new RasterizerState() { CullMode = CullMode.CullCounterClockwiseFace, FillMode = FillMode.Solid };
public RasterizerState rs_cull_cw_solid = new RasterizerState() { CullMode = CullMode.CullClockwiseFace, FillMode = FillMode.Solid };

You can also draw the pixels by the depth from back to front if you wanted via the depth buffer.

public DepthStencilState ds_standard_less = new DepthStencilState() { DepthBufferEnable = true, DepthBufferFunction = CompareFunction.Less };
public DepthStencilState ds_standard_more = new DepthStencilState() { DepthBufferEnable = true, DepthBufferFunction = CompareFunction.Greater };

It worked :slight_smile:

2 Likes

I gave implementing this a shot.

Here is what i came up with i basically stenciled the larger sphere to use on the smaller one and depth stenciled the smaller one against the larger Though i just used a single color float component for depth so its not very accurate but it worked in principle. Technically i should only stencil a area that covers the surface exactly but this was just a proof of concept test.

So these are both 3d objects i used one to stencil a chunk out of the second then the second with a second rendertarget to limit the back face of the first and drew it.

My draw method looks like this.

        protected override void Draw(GameTime gameTime)
        {
            Gu.device.SetRenderTarget(renderTarget1);
            GraphicsDevice.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, new Color(1f,1f,1f,1f), 1f, 0);

            // draw sphere littles depth to the rt for later comparison.
            Gu.device.DepthStencilState = ds_standard_less;
            SetTriangleEffectTechnique("DepthDraw"); // this is a single object depth stencil render
            DrawLittle();

            // draw the big spheres depth to rendertarget2
            Gu.device.SetRenderTarget(renderTarget2);
            DrawBig();

            // set the backbuffer
            GraphicsDevice.SetRenderTarget(null);
            // clear the backbuffer to blue
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // draw the depth stenciled sphere.
            SetTriangleEffectTechnique("DepthStencilDraw");
            SetTriangleEffectDepthTexture(renderTarget1);
            Gu.device.RasterizerState = rs_cull_ccw_solid;
            DrawBig();

            // draw the little sphere backfaced only were the stenciling is
            SetTriangleEffectTechnique("StencilDraw");
            SetTriangleEffectDepthTexture(renderTarget2);
            Gu.device.RasterizerState = rs_cull_cw_solid;
            DrawLittle();

            // draw rt 1 visibly for display.
            SetTriangleEffectTechnique("TriangleDraw");
            Gu.device.RasterizerState = rs_cull_cw_solid;
            DrawVisibleRt1();

            base.Draw(gameTime);
        }

the pixel shaders are fairly basic.

#if OPENGL
#define SV_POSITION POSITION
#define VS_SHADERMODEL vs_3_0
#define PS_SHADERMODEL ps_3_0
#else
#define VS_SHADERMODEL vs_4_0_level_9_1
#define PS_SHADERMODEL ps_4_0_level_9_1
#endif

Texture2D TextureA; // primary texture.
sampler TextureSamplerA = sampler_state
{
    texture = <TextureA>;
    //magfilter = LINEAR; //minfilter = LINEAR; //mipfilter = LINEAR; //AddressU = mirror; //AddressV = mirror; 
};

Texture2D StencilDepthTextureB; // depth texture.
sampler StencilDepthTextureSamplerB = sampler_state
{
    texture = <StencilDepthTextureB>;
    //magfilter = LINEAR; //minfilter = LINEAR; //mipfilter = LINEAR; //AddressU = mirror; //AddressV = mirror; 
};

matrix World;
matrix View;
matrix Projection;

float NearPlane;
float FarPlane;

float DotProduct(float3 lightPos, float3 pos3D, float3 normal)
{
    float3 lightDir = normalize(pos3D - lightPos);
    return dot(-lightDir, normal);
}

//_______________________________________________________________
// techniques 
// TriangleDraw   ,  draws a triangles via position texture.
//
//_______________________________________________________________
struct VsInputQuad
{
    float4 Position : POSITION0;
    float2 TexureCoordinateA : TEXCOORD0;
};
struct VsOutputQuad
{
    float4 Position : SV_Position;
    float2 TexureCoordinateA : TEXCOORD0;
};


// ____________________________
VsOutputQuad VertexShaderQuadDraw(VsInputQuad input)
{
    VsOutputQuad output;
    float4 pos = mul(input.Position, World);
    float4x4 vp = mul(View, Projection);
    output.Position = mul(pos, vp);
    output.TexureCoordinateA = input.TexureCoordinateA;
    return output;
}

float4 PixelShaderQuadDraw(VsOutputQuad input) : COLOR0
{
    float4 result = tex2D(TextureSamplerA, input.TexureCoordinateA); // *input.Color;
    return result;
}
technique TriangleDraw
{
    pass
    {
        VertexShader = compile VS_SHADERMODEL VertexShaderQuadDraw();
        PixelShader = compile PS_SHADERMODEL PixelShaderQuadDraw();
    }
}

//_______________________________________________________________
// techniques 
// Depth Draw  , this assigns the depth to the color.b or z of the color component.
//
//_______________________________________________________________
struct VsInputDepth
{
    float4 Position : POSITION0;
};
struct VsOutputDepth
{
    float4 Position : SV_Position;
    float Depth : TEXCOORD0;
};
VsOutputDepth VertexShaderDepthDraw(VsInputDepth input)
{
    VsOutputDepth output;
    float4 pos = mul(input.Position, World);
    float4x4 vp = mul(View, Projection);
    output.Position = mul(pos, vp);
    output.Depth = output.Position.z / (FarPlane + NearPlane);
    return output;
}

float4 PixelShaderDepthDraw(VsOutputDepth input) : COLOR0
{
    float4 result = float4(1.0f - input.Depth, 0.0f, input.Depth, 1.0f);
    return result;
}
technique DepthDraw
{
    pass
    {
        VertexShader = compile VS_SHADERMODEL VertexShaderDepthDraw();
        PixelShader = compile PS_SHADERMODEL PixelShaderDepthDraw();
    }
}

//_______________________________________________________________
// techniques 
// Depth Stencil Draw  
//
//_______________________________________________________________
struct VsInputDepthStencil
{
    float4 Position : POSITION0;
    float2 TexureCoordinateA : TEXCOORD0;
};
struct VsOutputDepthStencil
{
    float4 Position : SV_Position;
    float2 TexureCoordinateA : TEXCOORD0;
    float4 Pos2DAsSeenByCamera : TEXCOORD1;
};
VsOutputDepthStencil VertexShaderDepthStencilDraw(VsInputDepthStencil input)
{
    VsOutputDepthStencil output;
    float4 pos = mul(input.Position, World);
    float4x4 vp = mul(View, Projection);
    output.Position = mul(pos, vp);
    float depth = output.Position.z / (FarPlane + NearPlane);
    output.TexureCoordinateA = input.TexureCoordinateA;
    output.Pos2DAsSeenByCamera = float4(output.Position.x, output.Position.y, depth, output.Position.w);
    return output;
}
float4 PixelShaderDepthStencilDraw(VsOutputDepthStencil input) : COLOR0
{
    float4 col = tex2D(TextureSamplerA, input.TexureCoordinateA);

    float2 ProjectedTexCoords;
    ProjectedTexCoords[0] = input.Pos2DAsSeenByCamera.x / input.Pos2DAsSeenByCamera.w / 2.0f + 0.5f;
    ProjectedTexCoords[1] = -input.Pos2DAsSeenByCamera.y / input.Pos2DAsSeenByCamera.w / 2.0f + 0.5f;
    float previousDepth = tex2D(StencilDepthTextureSamplerB, ProjectedTexCoords).z;

    float4 result = col;
    if (previousDepth < input.Pos2DAsSeenByCamera.z)
        clip(-1.0f);
    return result;
}
technique DepthStencilDraw
{
    pass
    {
        VertexShader = compile VS_SHADERMODEL VertexShaderDepthStencilDraw();
        PixelShader = compile PS_SHADERMODEL PixelShaderDepthStencilDraw();
    }
}


//_______________________________________________________________
// techniques 
// Stenciled Draw  
//
//_______________________________________________________________
float4 PixelShaderStencilDraw(VsOutputDepthStencil input) : COLOR0
{
    float4 col = tex2D(TextureSamplerA, input.TexureCoordinateA);
    col.a = 1.0f;

    float2 ProjectedTexCoords;
    ProjectedTexCoords[0] = input.Pos2DAsSeenByCamera.x / input.Pos2DAsSeenByCamera.w / 2.0f + 0.5f;
    ProjectedTexCoords[1] = -input.Pos2DAsSeenByCamera.y / input.Pos2DAsSeenByCamera.w / 2.0f + 0.5f;
    float previousDepth = tex2D(StencilDepthTextureSamplerB, ProjectedTexCoords).z;

    float4 result = col;
    if (previousDepth < 0.001f || previousDepth > 0.99f)
        clip(-1.0f);
    return result;
}
technique StencilDraw
{
    pass
    {
        VertexShader = compile VS_SHADERMODEL VertexShaderDepthStencilDraw();
        PixelShader = compile PS_SHADERMODEL PixelShaderStencilDraw();
    }
}