Skip to content

Commit 07d6b6c

Browse files
GLTF: support injesting xml
1 parent 18373fd commit 07d6b6c

5 files changed

Lines changed: 350 additions & 18 deletions

File tree

lib/classes/LLGLTFMaterial.ts

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,43 @@ export class LLGLTFMaterial
1111
{
1212
if (data !== undefined)
1313
{
14-
const header = data.subarray(0, 18).toString('utf-8');
15-
if (header.length !== 18 || header !== '<? LLSD/Binary ?>\n')
16-
{
17-
throw new Error('Failed to parse LLGLTFMaterial');
18-
}
19-
20-
const body = new LLSD.Binary(Array.from(data.subarray(18)), 'BINARY');
21-
const llsd = LLSD.LLSD.parseBinary(body);
22-
if (!llsd.result)
14+
const result = LLGLTFMaterial.parseEnvelope(data);
15+
if (result === undefined || result === null)
2316
{
2417
throw new Error('Failed to decode LLGLTFMaterial');
2518
}
26-
if (llsd.result.type)
19+
if (result.type)
2720
{
28-
this.type = String(llsd.result.type);
21+
this.type = String(result.type as unknown);
2922
}
30-
if (llsd.result.version)
23+
if (result.version)
3124
{
32-
this.version = String(llsd.result.version);
25+
this.version = String(result.version as unknown);
3326
}
34-
if (llsd.result.data)
27+
if (result.data)
3528
{
36-
const assetData = String(llsd.result.data);
29+
const assetData = String(result.data as unknown);
3730
this.data = JSON.parse(assetData);
3831
}
3932
}
4033
}
34+
35+
private static parseEnvelope(data: Buffer): { type?: unknown; version?: unknown; data?: unknown } | undefined
36+
{
37+
const header = data.subarray(0, 18).toString('utf-8');
38+
if (header === '<? LLSD/Binary ?>\n')
39+
{
40+
const body = new LLSD.Binary(Array.from(data.subarray(18)), 'BINARY');
41+
const llsd = LLSD.LLSD.parseBinary(body);
42+
return llsd?.result as { type?: unknown; version?: unknown; data?: unknown };
43+
}
44+
45+
const text = data.toString('utf-8').trimStart();
46+
if (text.startsWith('<'))
47+
{
48+
return LLSD.LLSD.parseXML(text) as { type?: unknown; version?: unknown; data?: unknown };
49+
}
50+
51+
throw new Error('Failed to parse LLGLTFMaterial');
52+
}
4153
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { LLGLTFResolvedMaterial } from './LLGLTFResolvedMaterial';
2+
import { LLGLTFMaterialOverride } from './LLGLTFMaterialOverride';
3+
import type { LLGLTFMaterialData } from './LLGLTFMaterialData';
4+
import * as assert from 'assert';
5+
6+
const base: LLGLTFMaterialData = {
7+
materials: [
8+
{
9+
pbrMetallicRoughness: {
10+
baseColorFactor: [1, 0, 0, 1],
11+
metallicFactor: 0.25,
12+
roughnessFactor: 0.75,
13+
baseColorTexture: {
14+
index: 0,
15+
extensions: {
16+
KHR_texture_transform: {
17+
offset: [0.1, 0.2],
18+
scale: [2, 2],
19+
rotation: 0.5
20+
}
21+
}
22+
},
23+
metallicRoughnessTexture: { index: 1 }
24+
},
25+
normalTexture: { index: 2 },
26+
emissiveFactor: [0.3, 0.3, 0.3],
27+
emissiveTexture: { index: 3 },
28+
alphaMode: 'MASK',
29+
alphaCutoff: 0.4,
30+
doubleSided: true
31+
}
32+
],
33+
textures: [{ source: 0 }, { source: 1 }, { source: 2 }, { source: 3 }],
34+
images: [
35+
{ uri: 'a6edc906-2f9f-5fb2-a373-efac406f0ef2' },
36+
{ uri: '2f70a4f7-4ece-48d2-8963-32192608067d' },
37+
{ uri: '45a45cc0-463c-49dd-9133-5202399a16d4' },
38+
{ uri: '39bf5b2b-0619-4892-872c-024e2f601684' }
39+
]
40+
};
41+
42+
describe('LLGLTFResolvedMaterial', () =>
43+
{
44+
it('flattens a base material', () =>
45+
{
46+
const resolved = LLGLTFResolvedMaterial.fromMaterialData(base);
47+
assert.deepEqual(resolved.baseColorFactor, [1, 0, 0, 1]);
48+
assert.equal(resolved.metallicFactor, 0.25);
49+
assert.equal(resolved.roughnessFactor, 0.75);
50+
assert.equal(resolved.alphaMode, 2);
51+
assert.equal(resolved.alphaCutoff, 0.4);
52+
assert.equal(resolved.doubleSided, true);
53+
assert.equal(resolved.baseColorTexture.textureID, 'a6edc906-2f9f-5fb2-a373-efac406f0ef2');
54+
assert.deepEqual(resolved.baseColorTexture.transform.offset, [0.1, 0.2]);
55+
assert.deepEqual(resolved.baseColorTexture.transform.scale, [2, 2]);
56+
assert.equal(resolved.baseColorTexture.transform.rotation, 0.5);
57+
assert.equal(resolved.metallicRoughnessTexture.textureID, '2f70a4f7-4ece-48d2-8963-32192608067d');
58+
assert.equal(resolved.normalTexture.textureID, '45a45cc0-463c-49dd-9133-5202399a16d4');
59+
assert.equal(resolved.emissiveTexture.textureID, '39bf5b2b-0619-4892-872c-024e2f601684');
60+
});
61+
62+
it('applies an override on top of a base', () =>
63+
{
64+
const over = new LLGLTFMaterialOverride();
65+
over.metallicFactor = 0.9;
66+
over.baseColor = [0, 1, 0, 1];
67+
over.textures = [null, undefined as unknown as string, 'cccccccc-cccc-cccc-cccc-cccccccccccc', undefined as unknown as string];
68+
69+
const resolved = LLGLTFResolvedMaterial.resolve(base, over);
70+
assert.equal(resolved.metallicFactor, 0.9);
71+
assert.equal(resolved.roughnessFactor, 0.75);
72+
assert.deepEqual(resolved.baseColorFactor, [0, 1, 0, 1]);
73+
assert.equal(resolved.baseColorTexture.textureID, null);
74+
assert.equal(resolved.metallicRoughnessTexture.textureID, 'cccccccc-cccc-cccc-cccc-cccccccccccc');
75+
assert.equal(resolved.normalTexture.textureID, '45a45cc0-463c-49dd-9133-5202399a16d4');
76+
});
77+
78+
it('resolves an override with no base from defaults', () =>
79+
{
80+
const over = new LLGLTFMaterialOverride();
81+
over.roughnessFactor = 0.1;
82+
over.emissiveFactor = [1, 1, 1];
83+
84+
const resolved = LLGLTFResolvedMaterial.resolve(undefined, over);
85+
assert.equal(resolved.metallicFactor, 1);
86+
assert.equal(resolved.roughnessFactor, 0.1);
87+
assert.deepEqual(resolved.baseColorFactor, [1, 1, 1, 1]);
88+
assert.deepEqual(resolved.emissiveFactor, [1, 1, 1]);
89+
assert.equal(resolved.baseColorTexture.textureID, null);
90+
});
91+
});
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import type { LLGLTFMaterialData } from './LLGLTFMaterialData';
2+
import type { LLGLTFMaterialOverride } from './LLGLTFMaterialOverride';
3+
import { UUID } from './UUID';
4+
5+
export interface LLGLTFResolvedTransform
6+
{
7+
offset: [number, number];
8+
scale: [number, number];
9+
rotation: number;
10+
}
11+
12+
export interface LLGLTFResolvedTexture
13+
{
14+
textureID: string | null;
15+
transform: LLGLTFResolvedTransform;
16+
}
17+
18+
interface LLGLTFTextureInfoLike
19+
{
20+
index: number;
21+
extensions?: Record<string, unknown>;
22+
}
23+
24+
export class LLGLTFResolvedMaterial
25+
{
26+
public baseColorFactor: [number, number, number, number] = [1, 1, 1, 1];
27+
public emissiveFactor: [number, number, number] = [0, 0, 0];
28+
public metallicFactor: number = 1;
29+
public roughnessFactor: number = 1;
30+
public alphaMode: number = 0;
31+
public alphaCutoff: number = 0.5;
32+
public doubleSided: boolean = false;
33+
public baseColorTexture: LLGLTFResolvedTexture = LLGLTFResolvedMaterial.defaultTexture();
34+
public normalTexture: LLGLTFResolvedTexture = LLGLTFResolvedMaterial.defaultTexture();
35+
public metallicRoughnessTexture: LLGLTFResolvedTexture = LLGLTFResolvedMaterial.defaultTexture();
36+
public emissiveTexture: LLGLTFResolvedTexture = LLGLTFResolvedMaterial.defaultTexture();
37+
38+
public static resolve(base?: LLGLTFMaterialData, override?: LLGLTFMaterialOverride): LLGLTFResolvedMaterial
39+
{
40+
const resolved = base !== undefined ? LLGLTFResolvedMaterial.fromMaterialData(base) : new LLGLTFResolvedMaterial();
41+
if (override !== undefined)
42+
{
43+
resolved.applyOverride(override);
44+
}
45+
return resolved;
46+
}
47+
48+
public static fromMaterialData(data: LLGLTFMaterialData): LLGLTFResolvedMaterial
49+
{
50+
const resolved = new LLGLTFResolvedMaterial();
51+
const mat = data.materials?.[0];
52+
if (mat === undefined)
53+
{
54+
return resolved;
55+
}
56+
57+
const pbr = mat.pbrMetallicRoughness;
58+
if (pbr !== undefined)
59+
{
60+
if (pbr.baseColorFactor !== undefined && pbr.baseColorFactor.length === 4)
61+
{
62+
resolved.baseColorFactor = [pbr.baseColorFactor[0], pbr.baseColorFactor[1], pbr.baseColorFactor[2], pbr.baseColorFactor[3]];
63+
}
64+
if (pbr.metallicFactor !== undefined)
65+
{
66+
resolved.metallicFactor = pbr.metallicFactor;
67+
}
68+
if (pbr.roughnessFactor !== undefined)
69+
{
70+
resolved.roughnessFactor = pbr.roughnessFactor;
71+
}
72+
resolved.baseColorTexture = LLGLTFResolvedMaterial.resolveTexture(data, pbr.baseColorTexture);
73+
resolved.metallicRoughnessTexture = LLGLTFResolvedMaterial.resolveTexture(data, pbr.metallicRoughnessTexture);
74+
}
75+
76+
resolved.normalTexture = LLGLTFResolvedMaterial.resolveTexture(data, mat.normalTexture);
77+
resolved.emissiveTexture = LLGLTFResolvedMaterial.resolveTexture(data, mat.emissiveTexture);
78+
79+
if (mat.emissiveFactor !== undefined && mat.emissiveFactor.length === 3)
80+
{
81+
resolved.emissiveFactor = [mat.emissiveFactor[0], mat.emissiveFactor[1], mat.emissiveFactor[2]];
82+
}
83+
if (mat.alphaCutoff !== undefined)
84+
{
85+
resolved.alphaCutoff = mat.alphaCutoff;
86+
}
87+
resolved.doubleSided = mat.doubleSided === true;
88+
89+
switch (mat.alphaMode)
90+
{
91+
case 'BLEND':
92+
resolved.alphaMode = 1;
93+
break;
94+
case 'MASK':
95+
resolved.alphaMode = 2;
96+
break;
97+
default:
98+
resolved.alphaMode = 0;
99+
break;
100+
}
101+
102+
return resolved;
103+
}
104+
105+
public applyOverride(override: LLGLTFMaterialOverride): void
106+
{
107+
if (override.baseColor !== undefined && override.baseColor.length === 4)
108+
{
109+
this.baseColorFactor = [override.baseColor[0], override.baseColor[1], override.baseColor[2], override.baseColor[3]];
110+
}
111+
if (override.emissiveFactor !== undefined && override.emissiveFactor.length === 3)
112+
{
113+
this.emissiveFactor = [override.emissiveFactor[0], override.emissiveFactor[1], override.emissiveFactor[2]];
114+
}
115+
if (override.metallicFactor !== undefined)
116+
{
117+
this.metallicFactor = override.metallicFactor;
118+
}
119+
if (override.roughnessFactor !== undefined)
120+
{
121+
this.roughnessFactor = override.roughnessFactor;
122+
}
123+
if (override.alphaMode !== undefined)
124+
{
125+
this.alphaMode = override.alphaMode;
126+
}
127+
if (override.alphaCutoff !== undefined)
128+
{
129+
this.alphaCutoff = override.alphaCutoff;
130+
}
131+
if (override.doubleSided !== undefined)
132+
{
133+
this.doubleSided = override.doubleSided;
134+
}
135+
136+
const targets = [this.baseColorTexture, this.normalTexture, this.metallicRoughnessTexture, this.emissiveTexture];
137+
138+
if (override.textures !== undefined)
139+
{
140+
for (let i = 0; i < targets.length; i++)
141+
{
142+
const texture = override.textures[i];
143+
if (texture !== undefined)
144+
{
145+
targets[i].textureID = texture;
146+
}
147+
}
148+
}
149+
150+
if (override.textureTransforms !== undefined)
151+
{
152+
for (let i = 0; i < targets.length; i++)
153+
{
154+
const transform = override.textureTransforms[i];
155+
if (transform === undefined || transform === null)
156+
{
157+
continue;
158+
}
159+
if (transform.offset !== undefined && transform.offset.length === 2)
160+
{
161+
targets[i].transform.offset = [transform.offset[0], transform.offset[1]];
162+
}
163+
if (transform.scale !== undefined && transform.scale.length === 2)
164+
{
165+
targets[i].transform.scale = [transform.scale[0], transform.scale[1]];
166+
}
167+
if (transform.rotation !== undefined)
168+
{
169+
targets[i].transform.rotation = transform.rotation;
170+
}
171+
}
172+
}
173+
}
174+
175+
private static defaultTexture(): LLGLTFResolvedTexture
176+
{
177+
return {
178+
textureID: null,
179+
transform: {
180+
offset: [0, 0],
181+
scale: [1, 1],
182+
rotation: 0
183+
}
184+
};
185+
}
186+
187+
private static resolveTexture(data: LLGLTFMaterialData, info: LLGLTFTextureInfoLike | undefined): LLGLTFResolvedTexture
188+
{
189+
const resolved = LLGLTFResolvedMaterial.defaultTexture();
190+
if (info === undefined)
191+
{
192+
return resolved;
193+
}
194+
195+
const texture = data.textures?.[info.index];
196+
const source = texture?.source;
197+
if (source !== undefined)
198+
{
199+
const image = data.images?.[source];
200+
if (image !== undefined && 'uri' in image && image.uri !== undefined && image.uri !== UUID.zero().toString())
201+
{
202+
resolved.textureID = image.uri;
203+
}
204+
}
205+
206+
const transform = info.extensions?.KHR_texture_transform as { offset?: number[]; scale?: number[]; rotation?: number } | undefined;
207+
if (transform !== undefined)
208+
{
209+
if (transform.offset !== undefined && transform.offset.length === 2)
210+
{
211+
resolved.transform.offset = [transform.offset[0], transform.offset[1]];
212+
}
213+
if (transform.scale !== undefined && transform.scale.length === 2)
214+
{
215+
resolved.transform.scale = [transform.scale[0], transform.scale[1]];
216+
}
217+
if (transform.rotation !== undefined)
218+
{
219+
resolved.transform.rotation = transform.rotation;
220+
}
221+
}
222+
223+
return resolved;
224+
}
225+
}

lib/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,15 @@ import { LLGestureChatStep } from './classes/LLGestureChatStep';
105105
import { LLGestureStepType } from './enums/LLGestureStepType';
106106
import { LLLindenText } from './classes/LLLindenText';
107107
import { LLGLTFMaterial } from './classes/LLGLTFMaterial';
108+
import { LLGLTFMaterialOverride } from './classes/LLGLTFMaterialOverride';
109+
import { LLGLTFResolvedMaterial } from './classes/LLGLTFResolvedMaterial';
110+
import type { LLGLTFMaterialData } from './classes/LLGLTFMaterialData';
111+
import type { LLGLTFResolvedTexture, LLGLTFResolvedTransform } from './classes/LLGLTFResolvedMaterial';
108112
import { ExtendedMeshData } from './classes/public/ExtendedMeshData';
109113
import { ReflectionProbeData } from './classes/public/ReflectionProbeData';
110114
import { RenderMaterialData } from './classes/public/RenderMaterialData';
111115
import type { LLSettingsHazeConfig, LLSettingsTermConfig } from './classes/LLSettings';
112116
import { LLSettings } from './classes/LLSettings';
113117

114-
export type { LLSettingsTermConfig, LLSettingsHazeConfig, GlobalPosition, MapLocation };
115-
export { Bot, LoginParameters, ClientEvents, BVH, ChatSourceType, UUID, Vector3, Vector2, Utils, TextureEntry, LLWearable, LLLindenText, LLGLTFMaterial, LLGesture, LLGestureAnimationStep, LLGestureSoundStep, LLGestureChatStep, LLGestureWaitStep, LLSettings, ParticleSystem, ExtraParams, AgentFlags, BotOptionFlags, CompressedFlags, ControlFlags, DecodeFlags, InstantMessageEventFlags, InventoryItemFlags, LoginFlags, MessageFlags, ParcelInfoFlags, PacketFlags, RegionProtocolFlags, SoundFlags, TeleportFlags, RegionFlags, RightsFlags, ParticleDataFlags, TextureFlags, PrimFlags, ParcelFlags, SimAccessFlags, TextureAnimFlags, InventoryType, AssetType, FolderType, TransferStatus, SourcePattern, BlendFunc, PCode, Bumpiness, HoleType, LayerType, MappingType, PhysicsShapeType, ProfileShape, SculptType, Shininess, LLGestureStepType, ChatEvent, DisconnectEvent, FriendRequestEvent, FriendResponseEvent, GroupChatEvent, GroupChatSessionAgentListEvent, GroupChatSessionJoinEvent, GroupNoticeEvent, GroupInviteEvent, InstantMessageEvent, InventoryOfferedEvent, LureEvent, MapInfoRangeReplyEvent, MapInfoReplyEvent, ParcelInfoReplyEvent, RegionInfoReplyEvent, TeleportEvent, ScriptDialogEvent, EventQueueStateChangeEvent, FriendOnlineEvent, FriendRightsEvent, FriendRemovedEvent, ObjectPhysicsDataEvent, ParcelPropertiesEvent, NewObjectEvent, ObjectKilledEvent, ObjectUpdatedEvent, GroupProfileReplyEvent, AvatarPropertiesReplyEvent, Avatar, Friend, FlexibleData, LightData, LightImageData, GameObject, Material, Parcel, SculptData, MeshData, LLMesh, LLAnimation, LLAnimationJoint, LLAnimationJointKeyFrame, InventoryItem, TarReader, TarWriter, ExtendedMeshData, ReflectionProbeData, RenderMaterialData };
118+
export type { LLSettingsTermConfig, LLSettingsHazeConfig, GlobalPosition, MapLocation, LLGLTFMaterialData, LLGLTFResolvedTexture, LLGLTFResolvedTransform };
119+
export { Bot, LoginParameters, ClientEvents, BVH, ChatSourceType, UUID, Vector3, Vector2, Utils, TextureEntry, LLWearable, LLLindenText, LLGLTFMaterial, LLGLTFMaterialOverride, LLGLTFResolvedMaterial, LLGesture, LLGestureAnimationStep, LLGestureSoundStep, LLGestureChatStep, LLGestureWaitStep, LLSettings, ParticleSystem, ExtraParams, AgentFlags, BotOptionFlags, CompressedFlags, ControlFlags, DecodeFlags, InstantMessageEventFlags, InventoryItemFlags, LoginFlags, MessageFlags, ParcelInfoFlags, PacketFlags, RegionProtocolFlags, SoundFlags, TeleportFlags, RegionFlags, RightsFlags, ParticleDataFlags, TextureFlags, PrimFlags, ParcelFlags, SimAccessFlags, TextureAnimFlags, InventoryType, AssetType, FolderType, TransferStatus, SourcePattern, BlendFunc, PCode, Bumpiness, HoleType, LayerType, MappingType, PhysicsShapeType, ProfileShape, SculptType, Shininess, LLGestureStepType, ChatEvent, DisconnectEvent, FriendRequestEvent, FriendResponseEvent, GroupChatEvent, GroupChatSessionAgentListEvent, GroupChatSessionJoinEvent, GroupNoticeEvent, GroupInviteEvent, InstantMessageEvent, InventoryOfferedEvent, LureEvent, MapInfoRangeReplyEvent, MapInfoReplyEvent, ParcelInfoReplyEvent, RegionInfoReplyEvent, TeleportEvent, ScriptDialogEvent, EventQueueStateChangeEvent, FriendOnlineEvent, FriendRightsEvent, FriendRemovedEvent, ObjectPhysicsDataEvent, ParcelPropertiesEvent, NewObjectEvent, ObjectKilledEvent, ObjectUpdatedEvent, GroupProfileReplyEvent, AvatarPropertiesReplyEvent, Avatar, Friend, FlexibleData, LightData, LightImageData, GameObject, Material, Parcel, SculptData, MeshData, LLMesh, LLAnimation, LLAnimationJoint, LLAnimationJointKeyFrame, InventoryItem, TarReader, TarWriter, ExtendedMeshData, ReflectionProbeData, RenderMaterialData };

0 commit comments

Comments
 (0)