-
Notifications
You must be signed in to change notification settings - Fork 420
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support Rendering Replica models in the simulator (under CONSTRUCTION) #132
Conversation
-) fixed a couple of loggings; -) use join in corrade to generate the file path;
-) fix bug: numBytes -) load customized image (.rgb file) using file to memory mapping;
src/esp/assets/PTexMeshData.cpp
Outdated
Cr::Containers::Array<const char, Cr::Utility::Directory::MapDeleter> data = | ||
Cr::Utility::Directory::mapRead(rgbFile); | ||
const int dim = static_cast<int>(std::sqrt(data.size() / 3)); // square | ||
const size_t numBytes = io::fileSize(rgbFile); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to load the textures from the file. Such textures are not in common picture format (e.g., png, jpg, tga), but some customized format in binary.
In ReplicaSDK, they mapped a texture (rgbFile) to the memory, and uploaded it to a texture (atlas). The code is as follows.
const size_t numBytes = std::experimental::filesystem::file_size(rgbFile);
// Open file
int fd = open(std::string(rgbFile).c_str(), O_RDONLY, 0);
void* mmappedData = mmap(NULL, numBytes, PROT_READ, MAP_PRIVATE | MAP_POPULATE, fd, 0);
meshes[i]->atlas.Upload(mmappedData, GL_RGB, GL_UNSIGNED_BYTE);
munmap(mmappedData, numBytes);
close(fd);
@mosra : Do you think the current implementation is proper?
Do you have better ideas in terms of leveraging Magnum to do the same work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait wait but this is practically reverting #106 to what was there before, and that change was done in order to help with #84 (and besides that, reducing platform-specific code). The code you're showing is precisely what mapRead()
does if I see correctly, apart from the non-portable MAP_POPULATE
flag
Is there any issue with the way it was done using Directory::mapRead()
? Because in my testing I couldn't see any problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Allow me to reply this one first before I read the other comments.
I found an issue in the previous code that the "dim" was wrong, which resulted from "numBytes".
After reading your comment, assuming the mapRead() did exactly the same, then it means getting the "numBytes" using "data.size()" is incorrect.
What is the correct way to get this number?
I would change it back.
(I am not aware of the #106 or #84 since I was on leave for a couple of months. Good to know.)
Thank you very much for the comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh. The array returned from mapRead()
should have its size equal to number of bytes in that file (and my tests so far were confirming that). The rest of the code looks exactly the same, so that would mean mapRead()
returns a wrong size?
What does numBytes
give and what data.size()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good to know. It is also possible my testing code has some bugs.
Allow me to take another look.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just tested it again. The numbers are identical. There must be something wrong in my previous debugging code. My bad. I sincerely apologize for the confusion. Sorry, Vladimir.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem ;) In any case, I'm not claiming my implementation is bug-free either, so if you see something strange with it, let me know.
src/esp/assets/PTexMeshData.cpp
Outdated
Magnum::ImageView2D image(Magnum::PixelFormat::RGB8UI, {dim, dim}, data); | ||
renderingBuffers_[iMesh] | ||
->tex.setWrapping(Magnum::GL::SamplerWrapping::ClampToEdge) | ||
.setMagnificationFilter(Magnum::GL::SamplerFilter::Linear) | ||
.setMinificationFilter(Magnum::GL::SamplerFilter::Linear) | ||
// .setStorage(1, GL::TextureFormat::RGB8UI, image.size()) | ||
// .setStorage(1, Magnum::GL::TextureFormat::RGB8UI, image.size()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mosra :
The code in the master version commented it out.
Do I need to set the storage here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was commited this way by @msavva in October 2018, I don't know what was the original reason :) Generally, calling setSubImage()
without setStorage()
or setImage()
being called first will result in a GL error and nothing being done, so I guess that's what is happening here? (Unless the texture storage is being set up somewhere else, but I don't see any such code anywhere else.)
If you are on linux you can run the viewer with --magnum-gpu-validation on
and it'll tell you in the console output about any GL errors. (Mac doesn't implement GL debug output, so there you'd need to test for GL::Renderer::error()
manually by hand.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am developing everything on the Mac. I guess I will have to require a linux box. :)
So here, if I understand you correctly, I should actively call setStorage(). Correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, or change the below to setImage()
... but setStorage()
is the preferred thing to do.
Another question:
Do I understand it correctly? |
CC: @jstraub |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I got a bit frigtened by the reverted mmap change, can you elaborate? :) The other is just minor stuff, hinting at APIs you could use.
src/esp/assets/Asset.cpp
Outdated
} else if (endsWith(path, "mesh.ply")) { | ||
// Warning: | ||
// The order of if clause matters. cannot move "mesh.ply" before | ||
// "ptex_quad_mesh.ply" or "semantic_quad_mesh.ply" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would do
ASSERT(!endsWith("quad_mesh.ply"));
here instead of the comment :)
src/esp/assets/PTexMeshData.cpp
Outdated
Cr::Containers::Array<const char, Cr::Utility::Directory::MapDeleter> data = | ||
Cr::Utility::Directory::mapRead(rgbFile); | ||
const int dim = static_cast<int>(std::sqrt(data.size() / 3)); // square | ||
const size_t numBytes = io::fileSize(rgbFile); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait wait but this is practically reverting #106 to what was there before, and that change was done in order to help with #84 (and besides that, reducing platform-specific code). The code you're showing is precisely what mapRead()
does if I see correctly, apart from the non-portable MAP_POPULATE
flag
Is there any issue with the way it was done using Directory::mapRead()
? Because in my testing I couldn't see any problem.
src/esp/assets/ResourceManager.cpp
Outdated
// officially released Replica dataset | ||
return (Corrade::Utility::String::stripSuffix(filename, "mesh.ply") + | ||
"textures"); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see why the lambda is needed if it's used just on a single place :) Also there's Directory::path() to make this an oneliner:
const std::string atlasDir = Corrade::Utility::Directory::join(
Corrade::Utility::Directory::path(filename), "textures");
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new format is using "textures" as the folder name, while the old one was using "ptex_textures". Cannot put them in a single line.
Well, I can use Ternary Operator instead of the lambda.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh well, sorry, missed that part, thought it's just cutting off a different filename 🙈
src/esp/assets/PTexMeshData.cpp
Outdated
const std::string rgbFile = Corrade::Utility::Directory::join( | ||
atlasFolder_, std::to_string(iMesh) + "-color-ptex.rgb"); | ||
|
||
ASSERT(io::exists(rgbFile), Error : Cannot find the rgb file); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's CORRADE_ASSERT() btw, which could make esp/logging.h
obsolete -- and it supports printing everything that Utility::Debug can :)
ASSERT(io::exists(rgbFile), Error : Cannot find the rgb file); | |
CORRADE_ASSERT(io::exists(rgbFile), "Error : Cannot find the rgb file"); |
Also, for io::exists
there's Directory::exists() ... and and and
... 🤔 should I open a PR moving everything from esp/logging.h
and io/io.h
to builtin Corrade functionality? 😄 (cc: @msavva @erikwijmans)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logging.h and io.h are legacy.
I totally agree to move them to Corrade. But I do not think it is that urgent.
src/esp/assets/PTexMeshData.cpp
Outdated
Magnum::ImageView2D image(Magnum::PixelFormat::RGB8UI, {dim, dim}, data); | ||
renderingBuffers_[iMesh] | ||
->tex.setWrapping(Magnum::GL::SamplerWrapping::ClampToEdge) | ||
.setMagnificationFilter(Magnum::GL::SamplerFilter::Linear) | ||
.setMinificationFilter(Magnum::GL::SamplerFilter::Linear) | ||
// .setStorage(1, GL::TextureFormat::RGB8UI, image.size()) | ||
// .setStorage(1, Magnum::GL::TextureFormat::RGB8UI, image.size()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was commited this way by @msavva in October 2018, I don't know what was the original reason :) Generally, calling setSubImage()
without setStorage()
or setImage()
being called first will result in a GL error and nothing being done, so I guess that's what is happening here? (Unless the texture storage is being set up somewhere else, but I don't see any such code anywhere else.)
If you are on linux you can run the viewer with --magnum-gpu-validation on
and it'll tell you in the console output about any GL errors. (Mac doesn't implement GL debug output, so there you'd need to test for GL::Renderer::error()
manually by hand.)
No. SSBOs are since GL 4.3 and those aren't supported on macOS, as it's stuck on 4.1 + some extensions on top. |
@jstraub: |
Good to know. This is great news. |
What does the function do, exactly? To me it looks like for every face it finds out IDs of adjacent faces (plus some orientation encoding after)? If that's so, that could be done without the hashmap, without nested vectors, in |
Yes, I saw it, which is very cool if we can optimize their algorithm to O(N). But such code is not immediately available yet, and I have to debug and run the code hundreds of times a day. To ease my pain, such save/load functions are rather helpful at the moment. Hope you understand. We can treat them as temporary code here, and remove them later if they are indeed not necessary. |
Based on offline sync, we would like to only support the public released replica models.
- add gamma, saturation; - change the line_adjacency; - change the shader: texelFetch(...).r;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mosra :
Updated the code with the latest shaders from ReplicaSDK. They are modified so that they can be compiled with GL4.10.
However, it still does not work as before. The program gave a "grey" screen. You may download my branch and play it with any released Replica model.
I left a couple of questions in the code. Hope you can help me to accelerate the debugging. Thanks!
src/esp/gfx/PTexMeshDrawable.cpp
Outdated
|
||
void PTexMeshDrawable::draw(const Magnum::Matrix4& transformationMatrix, | ||
Magnum::SceneGraph::Camera3D& camera) { | ||
adjTex_.bind(1); | ||
adjFaces_.bind(1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mosra : adjFaces is the buffer texture, and it is bound to 1. Not sure if 1 is correct binding point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know -- I don't see any layout(binding = N)
on the shader side, or anything related in the constructor, for either this texture or the other, which leads me to think that this is the root of the problem. To be clear -- Magnum doesn't have any implicit binding points or anything, so you can't expect the shader to pick (samplers, uniforms, attributes) up unless you explicitly define the location / binding points for these as well. If things seemed to work with other shaders until now, it's just a lucky accident (as far as I can see, the GenericShader
also only ever uses one texture at a time).
(This comment is getting a bit long, so bear with me please.)
What's the designated way of doing this in Magnum (and what you are doing here differently) is, step-by-step (docs for reference -- look there for more details about each point):
-
For each uniform, defining
layout(location = N)
in the shader code, and then using the sameN
on C++ side. (Extensions needed for that are available on all systems we care about, so no need to save results ofuniformLocation()
on the C++ side.) Especially not usingsetUniform(uniformLocation("some string"), value)
each time in every frame, all those string operations are extremely wasteful. -
For each input attribute,
typedef
ing it in the header with location corresponding to thelayout()
qualifier in the shader code and type matching the type you are most likely to have the vertex data in. GL pads the remaining components with(0, 0, 0, 1)
so for example, if you pass aVector2
and havevec4
on the shader side, you'll get(x, y, 0, 1)
there -- no need to supply the0
and1
explicitly. Here you're usingVector4
and I don't see a reason to supply the fourth component, so it could beVector3
I think, saving 25% of memory. Of course thePTexMeshData
needs to be updated as well. -
For each sampler, either doing
layout(binding = N)
in the shader code or callingsetUniform(uniformLocation("sampler name"), N)
in the constructor. Here you need the latter, as macOS doesn't haveGL_ARB_shading_language_420pack
. Then, for each sampler providingbindSomething()
APIs on the C++ side, using the sameN
as a parameter totexture.bind()
. Here you havebindTexture()
just for one of the samplers and it has the binding point as a parameter, which doesn't make sense, and in the other case you're callingbind()
manually, with the1
having no relation to the shader at all. What I'm usually doing is defining an enum with the binding point IDs and then using those enums for both thebind()
andsetUniform()
calls -- so for example:enum: UnsignedInt { AtlasTextureUnit = 0, AdjacencyTextureUnit = 1 }; ... PTexMeshShader::PTexMeshShader() { setUniform(uniformLocation("atlasTex"), AtlasTextureUnit); setUniform(uniformLocation("meshAdjFaces"), AdjacencyTextureUnit); } PTexMeshDhader& PTexMeshShader::bindAtlasTexture(GL::Texture2D& texture) { texture.bind(AtlasTextureUnit); return *this; } PTexMeshDhader& PTexMeshShader::bindAdjacencyTexture(GL::BufferTexture& texture) { texture.bind(AdjacencyTextureUnit); return *this; }
This way the shader API on the C++ side is complete and you can be sure that you are always binding the right type of a texture to the right binding point. It's generally advised to choose mutually exclusive texture units for each shader (unless it's expected to use the same textures in each), as you save on texture rebinding when switching between different shaders. So e.g. if the generic shader makes use of units 0-3, the PTex shader would take units 4 and 5.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For each uniform, defining layout(location = N) in the shader code
I thought about it. But "Explicit uniform location" is not supported until 4.3. So I cannot use 'layout(location = N)' for the uniform in the shaders;
setUniform(uniformLocation("some string"), value) each time in every frame, all those string operations are extremely wasteful.
To save the call, I was thinking for certain uniforms, e.g., float "exposure". In C++, can we store a copy (denoted as "current_exposure") in the PTexMeshShader class to store the current value? In draw(), we first check if the exposure in the drawable is the same as the "current_exposure". If not, we setUniform(uniformLocation("exposure"), new_value)
, and let current_exposure = new_value;
It makes things complicated. But do you think it is necessary or it can reduce the overhead?
Magnum doesn't have any implicit binding points or anything, so you can't expect the shader to pick (samplers, uniforms, attributes) up unless you explicitly define the location / binding points for these as well.
Thank you for the clarification. If possible, I would even suggest you to put it in your docs. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you so much for the informative and invaluable comment! It saves me from a lot of guesswork on Magnum usage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But "Explicit uniform location" is not supported until 4.3.
Right, sorry. For some reason I thought it got in much earlier.
It makes things complicated. But do you think it is necessary or it can reduce the overhead?
Uh oh, i meant something else -- the uniformLocation()
, due to all its string operations, is the problem, not setUniform()
. So call it once in the constructor, save the ID and then do just setUniform(exposureUniform_, value)
. Check the performance plot in #151 -- just by going from GenericShader
(which calls uniformLocation()
every time) to magnum's Shaders::Flat
(which caches the locations), I was able to shave some additional milliseconds.
Long time ago I tried to cache the uniform values as well, but it didn't prove any measurable performance improvement. And the maintenance overhead was absolutely not worth it. What could make sense is going with UBOs, but those are known to be even slower than classic setUniform()
calls on Mac and some Intel GPUs, so ... ¯\_(ツ)_/¯
src/shaders/ptex-default-gl410.frag
Outdated
@@ -29,7 +29,7 @@ const uint FACE_MASK = 0x3FFFFFFF; | |||
|
|||
int GetAdjFace(int face, int edge, out int rot) { | |||
// uint data = meshAdjFaces[face * 4 + edge]; | |||
uint data = uint(texelFetch(meshAdjFaces, face * 4 + edge)); | |||
uint data = texelFetch(meshAdjFaces, face * 4 + edge).r; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mosra : Is it the correct way to fetch the unsigned int in the buffer texture?
BTW, please note: I changed the type of meshAdjFaces from sampleBuffer to usamplerBuffer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the shader compiler doesn't complain, yes :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The previous code will not be complained by the compiler either. But I doubt if that is correct. So I changed it.
currentMesh->abo.setData(adjFaces[iMesh], | ||
Magnum::GL::BufferUsage::StaticDraw); | ||
|
||
// experiment code (may not work): | ||
// using GL_LINES_ADJACENCY here to send quads to geometry shader |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mosra: I believe it is the most suspicious part.
The purpose is to send quads to geometry shader using GL_LINES_ADJACENCY.
The corresponding code in RelicaSDK is:
glDrawElements(GL_LINES_ADJACENCY, mesh.ibo.num_elements, mesh.ibo.datatype, 0);
Did I set it correctly using magnum?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think so.
Updated a version based on your comment. The code is suffering segfault now. I am debugging it. |
@mosra: (base) ~/temp/magnum/build/src/Magnum/GL/Test$ ./GLBufferTextureTest |
@mosra : When I use build.sh, the first thing it would do is to checkout the In this case, how can I test the simulator with the "magnum" master? |
Oh, that's great it's reproducible with the test alone. Can you get a backtrace of that, please? If that's a macOS driver bug, I could try to work around it on the engine side. For the submodules, you can pass |
Good to know. Thanks. The following is the bt:
|
The simulator was suffering the segfault as well with the latest magnum master.
It did not work either. It has nothing to do with the order sequence now. What was the change in the setBuffer? It may cause the failure here. But, if the setBuffer() was disabled, the segfault disappeared. Something is wrong with the mosra/magnum@7b43ab5 |
Sorry for the late replies, having too much on my plate :/
I'm pretty confident it's a macOS driver bug, not a problem with magnum. Nevertheless, the engine has to deal with the problem and make it non-crashy, somehow. The So we're looking at a driver bug. At this point I'm not sure about anything anymore, to be frank, and I guess I really need some access to a Mac to debug this. A totally random idea -- can you apply the following patch to the test and run it again? It's silly (and not required by the spec at all), but maybe that is the missing bit that causes the driver to blow up. diff --git a/src/Magnum/GL/Test/BufferTextureGLTest.cpp b/src/Magnum/GL/Test/BufferTextureGLTest.cpp
index 29d95d90d..104953f4f 100644
--- a/src/Magnum/GL/Test/BufferTextureGLTest.cpp
+++ b/src/Magnum/GL/Test/BufferTextureGLTest.cpp
@@ -207,7 +207,7 @@ void BufferTextureGLTest::setBufferEmptyFirst() {
#endif
BufferTexture texture;
- Buffer buffer;
+ Buffer buffer{Buffer::TargetHint::Texture};
texture.setBuffer(BufferTextureFormat::RGBA8UI, buffer);
MAGNUM_VERIFY_NO_GL_ERROR();
|
@mosra : Tried. Segfault. If you are a contingent worker at FB, can you ask the HR or related staff to get a macbook pro? See if they can ship one to your country. I do not think it is a problem! Sorry, I later have 1 hour meeting, will be back in the afternoon. Feel free to go to sleep:) |
Uh. Plan Z, then: can you try with plain textures? Similarly to what I did with the Primitive ID texture in the Primitive ID shader. I'm pretty sure this will fail on larger models and be slower than BufferTexture, but could be a workable short-term solution to get this running on Mac:
|
On Mac, I disabled the buffer texture in the shader. And it gave fairly good results: The current status: On Mac: Current workaround: Disable the buffer texture. Just not use the adjFaces. (Simulator segfaults with the magnum master, So I had to roll back to use the default magnum shipped with the simulator, and added "glBindBuffer, glTexBuffer" before the setBuffer (I still kept the setBuffer in front of setData() though)). Thinking About the segfault on Mac, and the magnum solution: For the Texture1D idea, I think it is a good idea to try. I would like to see at least some types of "buffer" work, and can pass the adjFaces to the shader correctly on Mac. But like you said, it would fail when the number of texels was big, plus it was slower. So I may not adopt it as our solution here. I will still use the texture buffer in this case. How do you think? |
Magnum has a framework for shader tests and it recently got improved even further for easier iteration. The most used builtin shaders are now all tested with it (see https://github.com/mosra/magnum/tree/master/src/Magnum/Shaders/Test). So if it would be feasible to craft small enough data (and expected output) to test on, then I think this would be a way to go. With the test scaffolding ready, the adjacency could be then (for example) verified by a specially crafted texture where the adjacent faces have a completely different color (and the test checks that the gradient in between is monotonic and doesn't include colors from wrong faces). Yes, it's some nontrivial work upfront, but it would be useful far beyond this bug I think -- later refactorings will be much easier to do, not to mention opening possibilities for benchmark-guided optimizations and saving us a lot of time when we get to making things work on Vulkan.
This is unfortunate, but I still believe Magnum does the right thing (and so I don't think the change should be reverted) -- the test that triggers the crash on Mac caused GL_INVALID_OPERATION on all drivers before, now it works everywhere except Mac.
So it's basically not used at all now, right? Which means if you'd
Unless I get access to a Mac, I'm afraid I can't debug this any further. I have some ideas, but with this way of "remote debugging" it would cost both you and me an extreme amount of time and I think that time could be spent more wisely elsewhere. |
@mosra: |
We divided it into a couple of PRs, which were committed in. |
…research#132) Limit requirements search scope to habitat_baselines folder.
Motivation and Context
Initialize the PR to get early feedbacks.
Purpose:
This PR is to upgrade the simulator so that it can render the Replica model released by FRL (https://github.com/facebookresearch/Replica-Dataset).
The master version can render the semantic scene of the replica model correctly. However, it cannot render the replica model (with the old ptex textures (.rgb format), or the new ones (.hdr format)) on either MacOS or Linux.
How Has This Been Tested
Types of changes
Checklist