Skip to content

Commit 92f0253

Browse files
SimonDarksideJviniciusjarina
authored andcommitted
Patch to resolve an issue where a model with empty normals calls MeshHelper.CalculateTangentFrames (MonoGame#8139)
*Note FIx currently skips geometry with empty normals as the CalculateNormals function is not working (failing tests)
1 parent 6169cac commit 92f0253

File tree

4 files changed

+123
-6
lines changed

4 files changed

+123
-6
lines changed

MonoGame.Framework.Content.Pipeline/Graphics/MeshHelper.cs

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Collections.ObjectModel;
43
using System.Diagnostics;
5-
using System.Linq;
64

75
namespace Microsoft.Xna.Framework.Content.Pipeline.Graphics
86
{
@@ -143,8 +141,22 @@ public static void CalculateTangentFrames(MeshContent mesh, string textureCoordi
143141
CalculateTangentFrames(geom, textureCoordinateChannelName, tangentChannelName, binormalChannelName);
144142
}
145143

144+
/// <summary>
145+
/// Generate the tangents and binormals (tangent frames) for each vertex in the mesh geometry.
146+
/// </summary>
147+
/// <param name="geom">The mesh geometry which will have add tangent and binormal channels added.</param>
148+
/// <param name="textureCoordinateChannelName">The Vector2 texture coordinate channel used to generate tangent frames.</param>
149+
/// <param name="tangentChannelName"></param>
150+
/// <param name="binormalChannelName"></param>
146151
public static void CalculateTangentFrames(GeometryContent geom, string textureCoordinateChannelName, string tangentChannelName, string binormalChannelName)
147152
{
153+
if (!geom.Vertices.Channels.Contains(VertexChannelNames.Normal(0)))
154+
{
155+
return;
156+
// TODO: We could generate the normals here, but it's not working.
157+
//MeshHelper.CalculateNormals(geom, true);
158+
}
159+
148160
var verts = geom.Vertices;
149161
var indices = geom.Indices;
150162
var channels = geom.Vertices.Channels;

Tests/Assets/Models/level1.fbx

2.24 MB
Binary file not shown.

Tools/MonoGame.Tools.Tests/ModelProcessorTests.cs

+49-4
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22
// This file is subject to the terms and conditions defined in
33
// file 'LICENSE.txt', which is part of this source code package.
44

5-
using System;
65
using Microsoft.Xna.Framework;
76
using Microsoft.Xna.Framework.Content.Pipeline;
87
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
9-
using Microsoft.Xna.Framework.Graphics;
10-
using NUnit.Framework;
118
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
9+
using NUnit.Framework;
10+
using System;
1211

1312
namespace MonoGame.Tests.ContentPipeline
1413
{
@@ -281,5 +280,51 @@ public void DefaultEffectTest()
281280

282281
//Assert.IsInstanceOf(typeof(SkinnedMaterialContent), output.Meshes[0].MeshParts[0].Material);
283282
}
283+
284+
[Test]
285+
/// <summary>
286+
/// Test to validate a model with missing normals does not throw an exception using the default ModelProcessor.
287+
/// </summary>
288+
public void MissingNormalsTestDefault()
289+
{
290+
string level1fbx = "Assets/Models/level1.fbx";
291+
var importer = new FbxImporter();
292+
var context = new TestImporterContext("TestObj", "TestBin");
293+
var nodeContent = importer.Import(level1fbx, context);
294+
295+
ModelProcessor processor = new ModelProcessor();
296+
var processorContext = new TestProcessorContext(TargetPlatform.Windows, "level1.xnb");
297+
298+
ModelContent output = null;
299+
// Validate that the processor does not throw an exception when normals are missing from the mesh
300+
Assert.DoesNotThrow(() => output = processor.Process(nodeContent, processorContext));
301+
302+
// Test some basics.
303+
Assert.NotNull(output);
304+
Assert.NotNull(output.Meshes);
305+
}
306+
307+
[Test]
308+
/// <summary>
309+
/// Test to validate a model with missing normals does not throw an exception using a custom ModelProcessor using MeshHelper.CalculateTangentFrames directly.
310+
/// </summary>
311+
public void MissingNormalsTestCustom()
312+
{
313+
string level1fbx = "Assets/Models/level1.fbx";
314+
var importer = new FbxImporter();
315+
var context = new TestImporterContext("TestObj", "TestBin");
316+
var nodeContent = importer.Import(level1fbx, context);
317+
318+
NormalMappingModelProcessor processor = new NormalMappingModelProcessor();
319+
var processorContext = new TestProcessorContext(TargetPlatform.Windows, "level1_costum.xnb");
320+
321+
ModelContent output = null;
322+
// Validate that the custom processor does not throw an exception when normals are missing from the mesh
323+
Assert.DoesNotThrow(() => output = processor.Process(nodeContent, processorContext));
324+
325+
// Test some basics.
326+
Assert.NotNull(output);
327+
Assert.NotNull(output.Meshes);
328+
}
284329
}
285-
}
330+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#region File Description
2+
//-----------------------------------------------------------------------------
3+
// NormalMappingModelProcessor.cs
4+
//
5+
// This file is subject to the terms and conditions defined in file 'LICENSE.txt', which is part of this source code package.
6+
// MonoGame - Copyright (C) The MonoGame Team
7+
//-----------------------------------------------------------------------------
8+
#endregion
9+
10+
#region Using Statements
11+
using Microsoft.Xna.Framework.Content.Pipeline;
12+
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
13+
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
14+
using System;
15+
16+
#endregion
17+
18+
namespace MonoGame.Tests.ContentPipeline
19+
{
20+
/// <summary>
21+
/// The NormalMappingModelProcessor is used to change the material/effect applied
22+
/// to a model. After going through this processor, the output model will be set
23+
/// up to be rendered with NormalMapping.fx.
24+
/// </summary>
25+
[ContentProcessor(DisplayName = "Normal Mapping Validation")]
26+
public class NormalMappingModelProcessor : ModelProcessor
27+
{
28+
public override ModelContent Process(NodeContent input,
29+
ContentProcessorContext context)
30+
{
31+
if (input == null)
32+
{
33+
throw new ArgumentNullException("input");
34+
}
35+
context.Logger.LogImportantMessage("processing: " + input.Name);
36+
PreprocessSceneHierarchy(input, context, input.Name);
37+
return base.Process(input, context);
38+
}
39+
40+
41+
/// <summary>
42+
/// Recursively calls MeshHelper.CalculateTangentFrames for every MeshContent
43+
/// object in the NodeContent scene.
44+
/// </summary>
45+
/// <param initialFileName="input">A node in the scene. The function should be called
46+
/// with the root of the scene.</param>
47+
private void PreprocessSceneHierarchy(NodeContent input,
48+
ContentProcessorContext context, string inputName)
49+
{
50+
MeshContent mesh = input as MeshContent;
51+
if (mesh != null)
52+
{
53+
MeshHelper.CalculateTangentFrames(mesh,
54+
VertexChannelNames.TextureCoordinate(0),
55+
VertexChannelNames.Tangent(0),
56+
VertexChannelNames.Binormal(0));
57+
}
58+
}
59+
}
60+
}

0 commit comments

Comments
 (0)