Skip to content
This repository was archived by the owner on Feb 13, 2025. It is now read-only.

Commit aaeeebd

Browse files
pknowlesnvbbarranpixeljetstreamakeley98NVmklefrancois
committed
Displacement-MicroMap-Toolkit squash
commit d139556f7b1171e8c3c0a8024d1b79412be9f4c4 Co-authored-by: Brian Barran <[email protected]> Co-authored-by: Christoph Kubisch <[email protected]> Co-authored-by: David Akeley <[email protected]> Co-authored-by: Martin-Karl Lefrancois <[email protected]> Co-authored-by: Mathias Heyer <[email protected]> Co-authored-by: Neil Bickford <[email protected]> Co-authored-by: Pascal Gautron <[email protected]> Co-authored-by: Pyarelal Knowles <[email protected]> Co-authored-by: Tristan Lorach <[email protected]>
1 parent 00fde4a commit aaeeebd

File tree

84 files changed

+1623
-616
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

84 files changed

+1623
-616
lines changed

.gitmodules

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
path = external/libpng
1111
url = https://git.code.sf.net/p/libpng/code
1212
shallow = true
13+
[submodule "external/heightmap_rtx"]
14+
path = external/heightmap_rtx
15+
url = https://github.com/NVIDIAGameWorks/heightmap_rtx.git
1316
[submodule "external/Displacement-MicroMap-SDK"]
1417
path = external/Displacement-MicroMap-SDK
1518
url = ../Displacement-MicroMap-SDK.git

CMakeLists.txt

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.13 FATAL_ERROR)
33
cmake_policy(SET CMP0079 NEW) # Allow linking with targets in other directories
44
project(
55
micromesh_toolkit
6-
VERSION 1.1
6+
VERSION 1.2
77
DESCRIPTION "Micromesh Tools"
88
LANGUAGES C CXX)
99

@@ -58,8 +58,7 @@ endif()
5858
#--------------------------------------------------------------------------------------------------
5959
# Package shared by all projects
6060

61-
# set(SUPPORT_AFTERMATH on)
62-
# _add_package_NsightAftermath()
61+
_add_package_NsightAftermath()
6362
_add_package_ZLIB()
6463
_add_package_VulkanSDK()
6564
_add_package_ShaderC()
@@ -141,6 +140,7 @@ target_link_libraries(png_static zlibstatic)
141140
target_include_directories(png_static PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/external/libpng> ${CMAKE_CURRENT_LIST_DIR}/external/libpng)
142141
###
143142
add_subdirectory(external/tinyexr)
143+
add_subdirectory(external/heightmap_rtx)
144144
add_subdirectory(meshops_core)
145145
add_subdirectory(meshops_bake)
146146
add_subdirectory(meshops_remesher)

README.md

+25-13
Original file line numberDiff line numberDiff line change
@@ -204,18 +204,14 @@ tools. It relies on `VK_NV_mesh_shader` to allow rasterized display of
204204
micromeshes. `VK_KHR_acceleration_structure` is required for baking micromaps.
205205
If available, `VK_NV_displacement_micromap` is used to render micromeshes with
206206
raytracing when choosing *Rendering -> RTX*. `VK_NV_displacement_micromap` was
207-
introduced with the Ada Lovelace architecture based GPUs. If you see a message
208-
about the missing extension, update to the latest driver (note: beta drivers are
209-
available at https://developer.nvidia.com/vulkan-driver).
210-
211-
> The rasterization of micromeshes, especially compressed is a bit of
212-
a more complex topic on its own. Therefore there will be a future
213-
dedicated sample that goes into the details of how it works
214-
and showcases more features, such as dynamic level-of-detail.
215-
We recommend to wait for this, rather than attempt to
216-
embed the code from the toolbox. The future sample will also
217-
provide more performant solutions and address compute-based
218-
rasterization as well.
207+
introduced with the RTX 40 Series Ada Lovelace architecture based GPUs. Previous
208+
RTX cards have support, but performance will be better with Ada. If you see a
209+
message about the missing extension, update to the latest driver (note: beta
210+
drivers are available at https://developer.nvidia.com/vulkan-driver).
211+
212+
> The rasterization of micromeshes, especially compressed, is a bit of
213+
a more complex topic on its own. A dedicated sample is available at
214+
https://github.com/nvpro-samples/vk_displacement_micromaps.
219215

220216
> At the time of writing `VK_NV_displacement_micromap` is in beta still, meaning
221217
the extension is subject to changes. Do not use its headers here in production
@@ -266,7 +262,23 @@ from an original input mesh in the [Micro-Mesh Asset Pipeline
266262
slide-deck](https://developer.download.nvidia.com/ProGraphics/nvpro-samples/slides/Micro-Mesh_Asset_Pipeline.pdf)
267263
as well as in [docs/asset_pipeline.md](docs/asset_pipeline.md)
268264

269-
## About the Latest Release (1.1)
265+
## About the Latest Release
266+
267+
Version 1.2
268+
269+
- Add eR11_unorm_packed_align32 support in meshopsOpCompressDisplacementMicromaps()
270+
- Add micromesh_tool --normal-textures-stem to generate normal maps
271+
- The micromesh_tool --resample-resolution argument now takes a 2D resolution
272+
- Faceted shading option when raytracing, or when missing normal maps
273+
- Update to the latest nvpro_core
274+
- Use heightmap_rtx to preview heightmap displacement when raytracing
275+
- Write textures in parallel when saving scenes
276+
- Fix baking heightmaps from base mesh materials - now using reference mesh
277+
- Fix support for loading \*.exr heightmaps
278+
- Filter outliers for dynamic range when using --height-textures-stem (experimental)
279+
- SW texture interpolation to preview heightmap displacement with rasterization
280+
281+
Version 1.1
270282

271283
- Heightmap rendering in micromesh_toolkit with dynamic LOD
272284
- Memory estimate for baking high resolution heightmaps in batches

automation/link_heightmaps.py

+38-5
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,19 @@
3030
parser.add_argument('--filter-out', nargs='+', help='Regex for material names to ignore')
3131
parser.add_argument('--copy-from', type=pathlib.Path, help='Source gltf file to use as a template. Takes precedence over the image filename search.')
3232
parser.add_argument('--match-one-image', action='store_true', help='A heightmap will be matched to an image and only added to materials sharing that image. Reduces false positives.')
33+
parser.add_argument('--match-one-material', action='store_true', help='Match a heightmap to at most one material, even if a color texture is shared by multiple materials.')
34+
parser.add_argument('--heightmap-regex', default=r'height|disp', help='Override the default regex for detecting heightmap-ish filenames')
35+
parser.add_argument('--image-name-weight', type=float, default=1.0, help='Weight given to the similarity of other images in a material')
36+
parser.add_argument('--material-name-weight', type=float, default=0.1, help='Weight given to the similarity of the material name')
37+
parser.add_argument('--match-materials-only', action='store_true', help='Match material names even if they have no other textures.')
3338
#parser.add_argument('--heightmaps', nargs='+', type=pathlib.Path, help='Provide heightmap names explicitly in the order of materials')
3439
args = parser.parse_args()
3540

41+
imageSuffixes = set(['.png', '.jpg', '.exr', '.bmp'])
42+
3643
# Return true if the filename could be a heightmap
3744
def heightmapishName(name):
38-
return re.search(r'height|disp', name, re.IGNORECASE)
45+
return re.search(args.heightmap_regex, name, re.IGNORECASE)
3946

4047
def find_in_object(obj, target_name, depth):
4148
if target_name in obj:
@@ -65,13 +72,15 @@ def find_ids(data, object_name, target_names, target_id):
6572
def heightmapMatchMetric(candidate, image, material_names):
6673
candidate_name = str(candidate.relative_to(filepath.parent))
6774
# Helps when height or displacement is a common term for all textures
68-
heightmap_terms = re.findall(r'height|disp', candidate_name, re.IGNORECASE)
75+
heightmap_terms = re.findall(args.heightmap_regex, candidate_name, re.IGNORECASE)
76+
# Remove the heightmap terms so they don't interfere with matching
77+
candidate_name = re.sub(args.heightmap_regex, '_', candidate_name)
6978
image_name = str(image.relative_to(filepath.parent))
7079
image_seq = SequenceMatcher(None, candidate_name, image_name)
7180
image_similarity = image_seq.ratio()
7281
match_seq = SequenceMatcher(None, candidate_name.lower(), '$'.join(material_names).lower())
7382
material_similarity = match_seq.ratio()
74-
score = image_similarity + material_similarity * 0.1 + len(heightmap_terms)
83+
score = image_similarity * args.image_name_weight + material_similarity * args.material_name_weight + len(heightmap_terms)
7584
if args.verbose:
7685
print('Image similarity score {} (filename {}, material name {}, heightmap terms {}):'.format(score, image_similarity, material_similarity * 0.1, len(heightmap_terms)))
7786
print(' {}'.format(candidate_name))
@@ -81,6 +90,14 @@ def heightmapMatchMetric(candidate, image, material_names):
8190
print()
8291
return score
8392

93+
# Returns material_ids, in descending order of their material names' similarity
94+
# to the candidate file name
95+
def orderMaterialSimilarity(candidate, materials, material_ids):
96+
candidate_name = str(candidate.relative_to(filepath.parent))
97+
candidate_name = re.sub(args.heightmap_regex, '_', candidate_name)
98+
sorted_pairs = sorted((SequenceMatcher(None, candidate_name, materials[material_id]['name']).ratio(), material_id) for material_id in material_ids)
99+
return [pair[1] for pair in reversed(sorted_pairs)]
100+
84101
def filterMaterialName(material_name):
85102
if args.filter and not any(re.search(pattern, material_name) for patterrn in args.filter):
86103
if args.verbose:
@@ -111,6 +128,7 @@ def filterMaterialName(material_name):
111128
path = filepath.parent / urllib.parse.unquote(uri)
112129
#if args.verbose: print('Existing image {}'.format(path))
113130
images.add(path)
131+
imageSuffixes.add(path.suffix)
114132
image_ids[path] = id
115133

116134
image_types = set()
@@ -147,7 +165,7 @@ def filterMaterialName(material_name):
147165
if args.verbose: print('Searching "{}"...'.format(str(dir)))
148166
for file in os.listdir(dir):
149167
candidate = dir / file
150-
if image.suffix == candidate.suffix and candidate not in images and heightmapishName(file):
168+
if candidate.suffix in imageSuffixes and candidate not in images and heightmapishName(file):
151169
if args.verbose: print('Found candidate {}'.format(str(candidate)))
152170
candidates.add(candidate)
153171

@@ -161,6 +179,18 @@ def filterMaterialName(material_name):
161179
material_names = list(data['materials'][material_id]['name'] for material_id in material_ids)
162180
matches += [(heightmapMatchMetric(candidate, image, material_names), candidate, image, material_ids)]
163181

182+
# HACK: this script started with only trying to match other texture
183+
# names. It's entirely possible there are no textures but material (and
184+
# even mesh) names would be fine matches. This quick workaround adds
185+
# "null" images so that material names can still be matched. This script
186+
# should really be rewritten more hollistically.
187+
if args.match_materials_only:
188+
all_material_ids = range(len(data['materials']))
189+
fake_image = filepath.parent / "null"
190+
for material_id in all_material_ids:
191+
material_names = [data['materials'][material_id]['name']]
192+
matches += [(heightmapMatchMetric(candidate, fake_image, material_names), candidate, fake_image, [material_id])]
193+
164194
# Assume the most similar heightmap names belong to the same materials as existing images in a greedy fashion
165195
unlinked_heightmaps = []
166196
unlinked_material_ids = set()
@@ -174,7 +204,7 @@ def filterMaterialName(material_name):
174204
elif not warn_multiple_matches:
175205
warn_multiple_matches = True
176206
print("Warning: adding the same heightmap multiple times consider enabling '--match-one-image'", file=sys.stderr)
177-
for material_id in set(material_ids) - unlinked_material_ids:
207+
for material_id in orderMaterialSimilarity(heightmap, data['materials'], set(material_ids) - unlinked_material_ids):
178208
unlinked_material_ids.add(material_id)
179209

180210
# Filter out materials with existing displacement
@@ -201,6 +231,9 @@ def filterMaterialName(material_name):
201231
unlinked_heightmaps += [(material_id, material_name, heightmap, image, sampler_id)]
202232
best_image[heightmap.name] = image
203233

234+
if args.match_one_image:
235+
break
236+
204237
had_changes = False
205238

206239
if not unlinked_heightmaps:
File renamed without changes.

external/heightmap_rtx

Submodule heightmap_rtx added at 86403b3

external/nvpro_core

Submodule nvpro_core updated 1110 files

meshops_bake/meshops_bake.cpp

+3-2
Original file line numberDiff line numberDiff line change
@@ -306,8 +306,9 @@ MESHOPS_API micromesh::Result MESHOPS_CALL meshopsOpBake(Context context, BakerO
306306
MESHOPS_LOGE(context, "Non-zero OpBake_input::ResamplerInput::texCoordIndex (%u) is not supported", texture.textureCoord);
307307
return micromesh::Result::eInvalidValue;
308308
}
309-
bool generatedTextureType = texture.textureType == TextureType::eQuaternionMap || texture.textureType == TextureType::eOffsetMap
310-
|| texture.textureType == TextureType::eHeightMap;
309+
bool generatedTextureType = texture.textureType == TextureType::eQuaternionMap
310+
|| texture.textureType == TextureType::eOffsetMap || texture.textureType == TextureType::eHeightMap
311+
|| texture.textureType == TextureType::eNewNormalMap;
311312
if(texture.texture)
312313
{
313314
if(generatedTextureType)

meshops_bake/meshops_bake_vk.cpp

+35-18
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737

3838
namespace meshops {
3939

40-
using namespace glsl_shared;
40+
using namespace shaders;
4141

4242
static bool getGlobalMinMax(ArrayView<const nvmath::vec2f> minMaxs, nvmath::vec2f& globalMinMax, bool filterZeroToOne, const uint32_t maxFilterWarnings);
4343

@@ -215,7 +215,7 @@ uint64_t BakerVK::estimateBaseGpuMemory(uint64_t distances, uint64_t triangles,
215215
// Persistent gpu memory used between batches
216216
uint64_t result = 0;
217217
result += nvh::align_up(sizeof(float) * distances, alignment); // m_distanceBuf
218-
result += nvh::align_up(sizeof(glsl_shared::Triangle) * triangles, alignment); // m_trianglesBuf
218+
result += nvh::align_up(sizeof(shaders::Triangle) * triangles, alignment); // m_trianglesBuf
219219
result += nvh::align_up(sizeof(nvmath::vec2f) * triangles, alignment); // m_triangleMinMaxBuf
220220
result += BakerMeshVK::estimateGpuMemory(triangles, vertices, requireDirectionBounds);
221221

@@ -820,9 +820,9 @@ uint64_t BakerMeshVK::estimateGpuMemory(uint64_t triangles, uint64_t vertices, b
820820

821821
// Buffers allocated in create() and createTessellated()
822822
uint64_t result = 0;
823-
result += nvh::align_up(sizeof(glsl_shared::CompressedVertex) * vertices, alignment); // vertices - raw position and compressed attributes
823+
result += nvh::align_up(sizeof(shaders::CompressedVertex) * vertices, alignment); // vertices - raw position and compressed attributes
824824
result += nvh::align_up(sizeof(nvmath::vec3ui) * triangles, alignment); // indices
825-
result += nvh::align_up(sizeof(glsl_shared::BakerMeshInfo), alignment);
825+
result += nvh::align_up(sizeof(shaders::BakerMeshInfo), alignment);
826826
if(requireDirectionBounds)
827827
{
828828
result += nvh::align_up(sizeof(nvmath::vec2f) * vertices, alignment); // directionBoundsBuf
@@ -983,7 +983,7 @@ void ResamplerPipeline::destroy(VkDevice device)
983983
//--------------------------------------------------------------------------------------------------
984984
//
985985
//
986-
void BakerPipeline::run(meshops::ContextVK& vk, const meshops::OpBake_input& input, glsl_shared::BakerPushConstants& pushConstants, bool finalBatch)
986+
void BakerPipeline::run(meshops::ContextVK& vk, const meshops::OpBake_input& input, shaders::BakerPushConstants& pushConstants, bool finalBatch)
987987
{
988988
nvh::ScopedTimer t("Run Compute Pass");
989989

@@ -1023,11 +1023,11 @@ void BakerPipeline::run(meshops::ContextVK& vk, const meshops::OpBake_input& inp
10231023
vk.resAllocator->finalizeAndReleaseStaging();
10241024
}
10251025

1026-
void ResamplerPipeline::run(meshops::ContextVK& vk,
1027-
const meshops::OpBake_input& input,
1028-
ArrayView<meshops::Texture> outputTextures,
1029-
glsl_shared::BakerPushConstants& pushConstants,
1030-
nvvk::Buffer& triangleMinMaxBuf)
1026+
void ResamplerPipeline::run(meshops::ContextVK& vk,
1027+
const meshops::OpBake_input& input,
1028+
ArrayView<meshops::Texture> outputTextures,
1029+
shaders::BakerPushConstants& pushConstants,
1030+
nvvk::Buffer& triangleMinMaxBuf)
10311031
{
10321032
nvh::ScopedTimer t("Run Resampler");
10331033
assert(outputTextures.size());
@@ -1062,21 +1062,38 @@ void ResamplerPipeline::run(meshops::ContextVK& vk,
10621062
generatingHeightmap = generatingHeightmap || input.resamplerInput[i].textureType == TextureType::eHeightMap;
10631063
}
10641064

1065-
// If we're generating a heightmap during resampling, we should scale the
1066-
// output by the bounds. This would need a second pass to first compute the
1067-
// values we would write. Instead, we can approximate the bounds by using the
1068-
// already-computed micromesh displacement values. These do get computed in
1069-
// meshops_bake.cpp, but this operation is not that common anyway.
1065+
// Heightmap generation is a byproduct and intended for use with the input
1066+
// base mesh (i.e. the micromap displaced output would be discarded). If we're
1067+
// generating a heightmap during resampling, we should scale the output by the
1068+
// bounds. This would need a second pass to first compute the values we would
1069+
// write. Instead, we can approximate the bounds by using the already-computed
1070+
// micromesh displacement values. These do get computed in meshops_bake.cpp,
1071+
// but this operation is not that common anyway.
10701072
pushConstants.globalMinMax = nvmath::vec2f(0.0f, 1.0f);
10711073
if(generatingHeightmap)
10721074
{
1075+
// Ignore the top and bottom 1% heights when choosing a heightmap scale.
10731076
auto minMaxs =
10741077
ArrayView(static_cast<nvmath::vec2f*>(vk.resAllocator->map(triangleMinMaxBuf)), input.baseMeshView.triangleCount());
1075-
if(!getGlobalMinMax(minMaxs, pushConstants.globalMinMax, false, 0))
1078+
auto minMaxFloatsView = ArrayView<float>(minMaxs);
1079+
std::vector<float> minMaxFloats(minMaxFloatsView.begin(), minMaxFloatsView.end());
1080+
std::sort(minMaxFloats.begin(), minMaxFloats.end());
1081+
size_t ignoreOutliers = minMaxFloats.size() / 100;
1082+
pushConstants.globalMinMax.x = minMaxFloats[ignoreOutliers];
1083+
pushConstants.globalMinMax.y = minMaxFloats[minMaxFloats.size() - 1 - ignoreOutliers];
1084+
vk.resAllocator->unmap(triangleMinMaxBuf);
1085+
1086+
// Print the scale - currently this is the only output the user has
1087+
auto globalBiasScale = BiasScalef::minmax_unit(pushConstants.globalMinMax);
1088+
LOGW("\nHeightmap range: [%f, %f] (bias %f, scale %f)\n", pushConstants.globalMinMax.x,
1089+
pushConstants.globalMinMax.y, globalBiasScale.bias, globalBiasScale.scale);
1090+
if(input.settings.fitDirectionBounds)
10761091
{
1077-
pushConstants.globalMinMax = {0.0f, 1.0f};
1092+
// When direction bounds are non-uniform, the direction vectors change.
1093+
// Even if the height values were rescaled, they will not work with the
1094+
// original mesh.
1095+
LOGW("Warning: heightmap will not work with the original base mesh due to --fit-direction-bounds\n");
10781096
}
1079-
vk.resAllocator->unmap(triangleMinMaxBuf);
10801097
}
10811098

10821099
// Create a list of resolutions. Each geometry instance will be rasterized at

0 commit comments

Comments
 (0)