donmccurdy / donmccurdy/glTF-Transform
Custom extension support cookbook
- Dominant language
- TypeScript
- Stars
- 2k
- Forks
- 206
- Avg merge
- 2d 2h
- Merged PRs (30d)
- 16
Description
This refers to the documentation and implementation of custom extensions, as described in the "Custom Extensions" section at https://gltf-transform.donmccurdy.com/extensions . This is not directly (or not _only_) a technical "feature request", so here is only a short summary of the "Feature Request" template points:
- Problem description: It's not really obvious how to add support for custom extensions.
- Desired solution: Make adding support for custom extensions easier on a technical level, or provide an easier-to-follow "cookbook"/tutorial".
---
The context is that I tried to implement basic support for the [`EXT_mesh_features`](https://github.com/KhronosGroup/glTF/pull/2082) extension. And even though this extension is (in its current form) structurally _relatively_ simple, it was not sooo easy to implement support for that.
Referring to the "Custom Extensions" section on the website, created from [this MD file](https://github.com/donmccurdy/glTF-Transform/blob/main/packages/docs/src/lib/pages/extensions.md): The "Emitter" example there is not really complete. The `ExtensionProperty` does not have a type parameter (so there should be an `IEmitter` interface definition). There are some issues with the properties and static properties of the `ExtensionProperty` class. (The details might be related to my cluelessness about TypeScript, technical details about `declare` and `override` and class initialization and whatnot, but ... for example, I wondered what that `init` method does, actually...). And beyond that, certain concepts may be obvious for someone who created them, and has implemented several extensions, and knows the underlying mechanisms of `property-graph` for the same reasons. But for others, "understanding" certain concepts just by looking at the examples may be difficult. A specific example is that of the role of the `TextureInfo` that is associated with a `Texture`. Yeah, it is used in many examples, but ... what is this about, exactly?
---
As a hopefully more constructive note: I started implementing that extension. And there are some **preliminary** notes that I dropped into my local files. These may end up in a similar form in the implementation, eventually. But they are written in a somewhat generic form, and thus, could count as some sort of "cookbook"/"tutorial":
For the `...Def` interfaces that define the JSON structure:
```
//============================================================================
// Interfaces for the JSON structure
//
// These interfaces reflect the structure of the JSON input, and can be
// derived directly from the JSON schema of the extension.
//
// The naming convention for these interfaces (and variables that refer
// to them) is that they end with `...Def`.
//
// In the 'read' method of the Extension class, they will be obtained
// from the `context.jsonDoc.json` in raw form, and translated into
// the "model" classes that are defined as
// export class MeshFeature extends ExtensionProperty {...}
//
// Note that textures are represented as a `GLTF.ITextureInfo`, with
// the `index` and `texCoord` properites. The "model" classes offer
// this as a `TextureInfo` object that is associated with the `Texture`
// object. This is used internally by glTF-Transform, to automatically
// do some sort of deduplication magic.
//
// In the 'write' method of the Extension class, these objects will be
// created from the "model" classes, and inserted into the JSON structure
// from the `context.jsonDoc.json`.
//
// The `GLTF.ITextureInfo` objects will be created with
// `context.createTextureInfoDef`, based on the `Texture´ and
// `TextureInfo` object from the model class.
//
interface MeshFeatureDef {
...
}
...
interface FeatureIdTextureDef extends GLTF.ITextureInfo {
channels?: number[];
}
//============================================================================
```
For the `I...` interfaces that define the _structure_ of the "model" classes (but that are not exposed!)
```
//============================================================================
// Interfaces for the model classes
//
// These correspond to the classes that are defined as the actual extension
// properties, as
// export class MeshFeature extends ExtensionProperty {...}
//
// The naming convention for these interfaces is that they start with `I...`.
// They all have to extend the `IProperty` interface.
//
// Note that the textures in these interfaces are using the actual
// `Texture` type of the public glTF-Transform API, and each `texture`
// has an associated `TextureInfo`. This is used internally by glTF-Transform
// for some deduplication magic or whatnot.
//
// These interfaces are NOT publicly visible. They only serve as the type
// pararameter for the `ExtensionProperty` class, which is the base
// for the actual "model" classes that are exposed to the user.
interface IMeshFeature extends IProperty {
...
}
...
interface IFeatureIdTexture extends IProperty {
channels: number[];
texture: Texture;
textureInfo: TextureInfo;
}
//============================================================================
```
For the actual "model" classes that are visible to the user:
```
//============================================================================
// The actual model classes
//
// These are exported, and visible to users.
//
// They offer accessor methods for the properties that are defined in
// the model class interfaces. Depending on the type of the properties,
// these accesor methods come in different flavors:
//
// - For "primitive" property types (like `number`, `boolean` and `string`),
// the implementations use `this.get(...)`/`this.set(...)`
// - For property types that correspond to other "model" classes,
// the implementations use `this.getRef(...)`/`this.setRef(...)`.
// - For property types that correspond to ARRAYS of "model" classes,
// the implementations don't offer "getters/setters", but instead,
// they offer `add/remove/list` methods, implemented based on
// `this.addRef(...)`/`this.removeRef(...)`/`this.listRefs(...)`.
//
// A special case is that of textures:
//
// Each texture in these classes is modeled as a property with the
// type `Texture`, and an associated `TextureInfo`. The `TextureInfo`
// can only be accessed with a `get` method, but not explicitly
// set: It is managed internally by glTF-Transform. So the for
// an `exampleTextureInfo: TextureInfo` property, there will only
// be a getter, implemented as
// ```
// getExampleTextureInfo(): TextureInfo | null {
// return this.getRef("exampleTexture") ?
// this.getRef("exampleTextureInfo") : null;
// }
// ```
export class MeshFeature extends ExtensionProperty {
static override EXTENSION_NAME = NAME;
override extensionName = NAME;
override propertyType = "MeshFeature";
override parentTypes = [PropertyType.PRIMITIVE];
protected init(): void {
// Nothing to do here...?
}
...
```
Further comments or hints could refer to the actual `Extension` class. This could be things that are "simple" in hindight - for example, point out that it has `create...` methods for the "model" classes, and how they are used in the `read` method. But it may also refer to information about where and why to call `context.setTextureInfo`, or details, like what _exactly_ is happening in a line like
```
const texture = context.textures[textureDefs[textureInfoDef.index].source!];
```
from [one example implementation](https://github.com/donmccurdy/glTF-Transform/blob/7b895a3fcfd5fbdb4e3abffd92fcde43a832fef8/packages/extensions/src/khr-materials-anisotropy/materials-anisotropy.ts#L83).
---
On a more technical side:
It would be nice if the redundancy between the `Something`/`ISomething`/`SomethingDef` definitions could be reduced. Or if something like the line about the `texture` above could be hidden behind some `const texture = context.getTextureFor(textureInfoDef)` or so. But I due to a lack of background knowledge, I sometimes have to _assume_ that there is no easier solution for certain things, so this is just a vague "Feature Wish" and not a "Feature Request" for now.
Contributor guide
Research direction
Start with packages/docs/src/lib/pages/extensions.md and the linked Custom Extensions section, then compare its Emitter example with the referenced KHR materials anisotropy implementation. The cookbook should explain the Def and model interfaces, ExtensionProperty initialization, TextureInfo relationships, and Extension read/write helpers, with enough guidance for implementing a custom extension.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- documentation
- Issue type
- Documentation
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 32/100