Skip to content

Deduplicate gltf materials during scene writing - #42

Merged
EliCDavis merged 4 commits into
EliCDavis:mainfrom
alexykot:feature/deduplicate-gltf-materials
Nov 30, 2024
Merged

Deduplicate gltf materials during scene writing#42
EliCDavis merged 4 commits into
EliCDavis:mainfrom
alexykot:feature/deduplicate-gltf-materials

Conversation

@alexykot

Copy link
Copy Markdown
Contributor

This is a new feature for GLTF writer, to deduplicate identical materials and only write the necessary number of unique materials, rather than a material per model. Different models using the same material will correctly reference the single instance of this material.

This improves size of the resulting GLTF/GLB file without changing the functionality in any way. This is adhering to GLTF spec - the separation of materials from models and explicit reference via material index encourages reuse of material objects imo.

Comment thread formats/gltf/model.go Outdated
Comment thread formats/gltf/model.go Outdated
Comment thread formats/gltf/model.go Outdated
Comment thread formats/gltf/model.go Outdated
Comment thread formats/gltf/material_tracker.go Outdated
Comment on lines +16 to +23
func (mt *materialTracker) findExistingMaterialID(mat PolyformMaterial) (*int, bool) {
for _, entry := range mt.entries {
if entry.polyMaterial.equal(mat) {
return &entry.index, true
}
}
return nil, false
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func (mt *materialTracker) findExistingMaterialID(mat PolyformMaterial) (*int, bool) {
for _, entry := range mt.entries {
if entry.polyMaterial.equal(mat) {
return &entry.index, true
}
}
return nil, false
}
func (mt *materialTracker) findExistingMaterialID(mat PolyformMaterial) int {
for _, entry := range mt.entries {
if entry.polyMaterial.equal(mat) {
return entry.index
}
}
return -1
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, why? There is no functional change here as far as I can see, beyond a micro-optimisation of returning one value instead of two. But I'm not sure it's worth it.

And this doesn't look Go-idiomatic to me. Is this your preferred style for this project?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can take it or leave it. Suggestion inspired by strings.Index. But if you prefer it that way it's fine

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Asked Claude about it. Prompt:

In the Go standard library - what style of item in the list presence check style is more prevalent - returning one value index: -1 if not found, or returning two values index: 0, found: false ?

His response:

Looking at the Go standard library, the two-value return pattern with a boolean flag is significantly more prevalent than returning -1 for "not found" cases. This pattern is consistently used across many standard library packages. Some examples:

strings.Index() returns -1 (single return)
map lookups return value, ok (two returns)
strings.Cut() returns text, ok (two returns)
Array/slice element type assertions return value, ok (two returns)
Type switches return value, ok (two returns)
Channel reads return value, ok (two returns)

The -1 convention in strings.Index() is actually somewhat of an outlier, likely kept for historical/compatibility reasons. For new code in Go, the two-value return with a boolean flag is considered more idiomatic.


So yeah, I suggest we leave it as is 🙂

Comment thread formats/gltf/material_tracker.go
Comment thread formats/gltf/model.go
Comment thread formats/gltf/model.go Outdated
Comment thread formats/gltf/writer.go
Comment on lines +337 to +339
if existingId, ok := w.matTracker.findExistingMaterialID(mat); ok {
return existingId, nil
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if existingId, ok := w.matTracker.findExistingMaterialID(mat); ok {
return existingId, nil
}
if existingId := w.matTracker.findExistingMaterialID(mat); existingId != -1 {
return &existingId, nil
}

@EliCDavis

Copy link
Copy Markdown
Owner

This is a great addition! One way to improve efficiency and avoid unnecessary checks is to change the signature of AddMaterial(mat PolyformMaterial) to take a pointer AddMaterial(mat *PolyformMaterial).

This way we can first check if the pointers are referencing the same thing, and if not, then we start doing our equality checks.

It's a breaking change to the API, but a very tiny one.

@alexykot

Copy link
Copy Markdown
Contributor Author

This is a great addition! One way to improve efficiency and avoid unnecessary checks is to change the signature of AddMaterial(mat PolyformMaterial) to take a pointer AddMaterial(mat *PolyformMaterial).

This way we can first check if the pointers are referencing the same thing, and if not, then we start doing our equality checks.

It's a breaking change to the API, but a very tiny one.

Cool. I didn't want to break the API, but if you think it's ok - I'll add that.

alexykot and others added 3 commits November 29, 2024 22:33
Co-authored-by: Eli Davis <eli.davis1995@gmail.com>
Co-authored-by: Eli Davis <eli.davis1995@gmail.com>

@alexykot alexykot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed comments

Comment thread formats/gltf/model.go Outdated
Comment thread formats/gltf/material_tracker.go Outdated
Comment on lines +16 to +23
func (mt *materialTracker) findExistingMaterialID(mat PolyformMaterial) (*int, bool) {
for _, entry := range mt.entries {
if entry.polyMaterial.equal(mat) {
return &entry.index, true
}
}
return nil, false
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, why? There is no functional change here as far as I can see, beyond a micro-optimisation of returning one value instead of two. But I'm not sure it's worth it.

And this doesn't look Go-idiomatic to me. Is this your preferred style for this project?

Comment thread formats/gltf/model.go Outdated
Comment thread formats/gltf/model.go
Comment thread formats/gltf/model.go
}

func (pm PolyformMaterial) equal(other PolyformMaterial) bool {
if pm.Name != other.Name {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An argument can be made that Name doesn't matter. They are arbitrary and optional and thus - don't need to be checked. I opted to check this as well, but practically it's of no use for the rendering process, so we could omit it for brevity. Lmk if you think it should be omitted.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point. I can't foresee why someone would care about a name, but I also don't want to potentially mess up an export for someone who builds logic around the material definition/name.

I think if we do remove this name check, we should introduce a new argument in the NewWriter() function, which would be a WriterConfig struct. I think for the time being, it'll have one field that'd be like, OptimizeMaterials . But it will allow us to add more options over time without breaking the API. If OptimizeMaterials is set to true, we do everything you've created in this PR. If it's set to false, it doesn't do anything

type WriterConfig struct {
    OptimizeMaterials bool
}

I might do this anyway. It would be a valid spot to add a new field like PanicOnInvalidDefinition, which would have addressed yesterdays PR.

Lemme know your thoughts!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would leave it as it is implemented now.

I assume in 99.9% of cases OptimizeMaterials is the desired behavior anyway. There is a microscopic performance hit on having to run the deduplication, but it's negligible, while the benefit of reduced model size is material. I struggle to imagine a use case where duplicated materials would be preferable. So putting it behind a flag that needs to be manually set, just to have it set in 99.9% of the times would just create additional lines in the caller implementation for no good reason.

If for whatever reason the user does want to have duplicates - the current shape of implementation gives them that ability. Assign unique names to materials, and they will be duplicated.

@alexykot
alexykot requested a review from EliCDavis November 29, 2024 23:15
Comment thread formats/gltf/model.go
}

func (pm PolyformMaterial) equal(other PolyformMaterial) bool {
if pm.Name != other.Name {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point. I can't foresee why someone would care about a name, but I also don't want to potentially mess up an export for someone who builds logic around the material definition/name.

I think if we do remove this name check, we should introduce a new argument in the NewWriter() function, which would be a WriterConfig struct. I think for the time being, it'll have one field that'd be like, OptimizeMaterials . But it will allow us to add more options over time without breaking the API. If OptimizeMaterials is set to true, we do everything you've created in this PR. If it's set to false, it doesn't do anything

type WriterConfig struct {
    OptimizeMaterials bool
}

I might do this anyway. It would be a valid spot to add a new field like PanicOnInvalidDefinition, which would have addressed yesterdays PR.

Lemme know your thoughts!

Comment thread formats/gltf/model.go
Comment on lines +158 to +159
r1, g1, b1, a1 := a.(color.Color).RGBA()
r2, g2, b2, a2 := b.(color.Color).RGBA()

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
r1, g1, b1, a1 := a.(color.Color).RGBA()
r2, g2, b2, a2 := b.(color.Color).RGBA()
r1, g1, b1, a1 := a.RGBA()
r2, g2, b2, a2 := b.RGBA()

@EliCDavis EliCDavis left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now it's fine where it is if you want to go ahead and merge it.

@alexykot

Copy link
Copy Markdown
Contributor Author

@EliCDavis I don't have write access to your repo, so I don't have a merge button to click 🙂

Can you push the button on your end please?

@EliCDavis
EliCDavis merged commit 1aecb51 into EliCDavis:main Nov 30, 2024
@alexykot
alexykot deleted the feature/deduplicate-gltf-materials branch December 1, 2024 18:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants