Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,34 @@ slot for all its faces, or to one slot per face. A skybox effect, referred to
as `effect:<scene>/<layer>/<effect>`, maps to one skybox slot: the theme's
skybox replaces its six faces.

A custom object, like the 3D terrain, maps some of its properties. An image
property maps to a texture slot, a 3D model property (like the model of
scattered models) to a model slot, a color property to a color slot, and the
texture of a terrain layer to a *material* slot, a texture with its normal map,
along with the property that takes the normal map:

```json
"scene:Game Scene/Terrain": {
"Layer1Texture": { "slot": "terrain.ground", "normalMap": "Layer1NormalMap" },
"Layer2Texture": { "slot": "terrain.road", "normalMap": "Layer2NormalMap" }
},
"scene:Game Scene/Grass": {
"BaseColor": "terrain.grass.base",
"TipColor": "terrain.grass.tip"
},
"scene:Game Scene/PineForest": {
"Model": "nature.tree"
}
```

A theme whose material has no normal map empties the normal map property: the
bumps of the starter's texture would not match the theme's. A theme model keeps
the other properties of the object, like the size of the scattered copies.
Custom objects don't have to be mapped, except those drawing a 3D model (like
scattered models), which are mapped or listed in `ignoredObjects` like 3D
models. The children of a custom object that are 3D models or cubes are mapped
too (list the placeholders of the terrain and the grass in `ignoredObjects`).

**When you add a 3D starter, add its `theme-slots.json`.** The build refuses to
publish a 3D starter without one, or one where a 3D model or cube is neither
mapped nor listed in `ignoredObjects`, and it names the object:
Expand Down
127 changes: 120 additions & 7 deletions scripts/lib/StarterThemer.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,18 @@
* origin?: {name: string, identifier: string},
* }} ThemeFile
*
* A material slot is a texture with, if the theme has one, its normal map. A
* color slot is a color, like "52;104;36".
* @typedef {ThemeFile & {
* kind: 'model' | 'texture',
* objectContent?: any,
* assetStoreId?: string,
* } | ThemeFile & {
* kind: 'material',
* normalMap?: ThemeFile,
* } | {
* kind: 'color',
* color: string,
* } | {
* kind: 'skybox',
* faces: Object.<string, ThemeFile>,
Expand Down Expand Up @@ -156,7 +164,9 @@ const getSizeRatio = (objectContent, themeContent) => {
* Each such object gets the theme's asset for that slot: a 3D model takes the
* theme model's dimensions, rotation, material and animations while keeping
* its name, behaviors, variables, instances, origin and center; a cube face
* points at the theme image; a skybox effect points at the six theme images.
* points at the theme image; a skybox effect points at the six theme images;
* a property of a custom object points at the theme image (and the normal map
* of a material) or model, or takes the theme color.
*
* Each theme file becomes a resource named like in the asset store, and the
* resources of the starter that nothing uses anymore are removed.
Expand Down Expand Up @@ -188,14 +198,23 @@ const applyThemeToStarter = (projectObject, starterThemeSlots, theme) => {

/**
* Add a file of a slot to the project as a resource configured like the
* resource it replaces, and return its name.
* resource it replaces, and return its name. Without a resource to replace
* (an empty property of a custom object), it is configured like
* `defaultResource`, if given.
* @param {string} replacedResourceName
* @param {string} slotId
* @param {ThemeFile} themeFile
* @param {any=} defaultResource
* @returns {string | null}
*/
const useThemeResource = (replacedResourceName, slotId, themeFile) => {
const replacedResource = resourceByName.get(replacedResourceName);
const useThemeResource = (
replacedResourceName,
slotId,
themeFile,
defaultResource
) => {
const replacedResource =
resourceByName.get(replacedResourceName) || defaultResource;
if (!replacedResource) return null;

// A resource of the starter can already have that name: number the new one.
Expand Down Expand Up @@ -225,10 +244,94 @@ const applyThemeToStarter = (projectObject, starterThemeSlots, theme) => {

appliedSlots.add(slotId);
themeResourceNames.add(name);
replacedResourceNames.add(replacedResourceName);
if (replacedResourceName) replacedResourceNames.add(replacedResourceName);
return name;
};

/**
* Theme the properties of a custom object: an image resource takes the
* texture of a texture or material slot, along with the normal map of the
* material if the starter maps one; a 3D model resource takes the model of
* a model slot; a color takes the color of a color slot.
* @param {any} objectContent
* @param {Object.<string, string | import('./ThemeSlots.js').MaterialMapping>} mapping
* @returns {boolean} Whether a property changed.
*/
const themeCustomObjectProperties = (objectContent, mapping) => {
let changed = false;
Object.entries(mapping).forEach(([property, propertyMapping]) => {
const slotId =
typeof propertyMapping === 'string'
? propertyMapping
: propertyMapping.slot;
const slot = theme.slots[slotId];
if (
slot &&
slot.kind === 'color' &&
typeof propertyMapping === 'string'
) {
objectContent[property] = slot.color;
appliedSlots.add(slotId);
changed = true;
return;
}
if (
slot &&
slot.kind === 'model' &&
typeof propertyMapping === 'string'
) {
// The custom object keeps its own settings for the model (like the
// size of scattered copies): only the model changes.
const themeModelResourceName = useThemeResource(
String(objectContent[property] || ''),
slotId,
slot,
{ kind: 'model3D', metadata: '', userAdded: false }
);
if (!themeModelResourceName) return;
objectContent[property] = themeModelResourceName;
changed = true;
return;
}
if (!slot || (slot.kind !== 'texture' && slot.kind !== 'material')) {
missingSlots.add(slotId);
return;
}

const themeTextureResourceName = useThemeResource(
String(objectContent[property] || ''),
slotId,
slot,
{ kind: 'image', metadata: '', smoothed: true, userAdded: false }
);
if (!themeTextureResourceName) return;
objectContent[property] = themeTextureResourceName;
changed = true;
if (typeof propertyMapping === 'string') return;

const normalMapProperty = propertyMapping.normalMap;
const normalMapResourceName = String(
objectContent[normalMapProperty] || ''
);
if (slot.kind === 'material' && slot.normalMap) {
objectContent[normalMapProperty] =
useThemeResource(
normalMapResourceName,
slotId,
slot.normalMap,
resourceByName.get(themeTextureResourceName)
) || '';
} else {
// The bumps of another texture would not match the theme texture.
objectContent[normalMapProperty] = '';
if (normalMapResourceName) {
replacedResourceNames.add(normalMapResourceName);
}
}
});
return changed;
};

Object.entries(starterThemeSlots.objects || {}).forEach(
([objectPath, mapping]) => {
const object = objectsByPath.get(objectPath);
Expand Down Expand Up @@ -274,12 +377,14 @@ const applyThemeToStarter = (projectObject, starterThemeSlots, theme) => {
if (object.type === 'Scene3D::Cube3DObject') {
let changed = false;
Object.keys(cubeFaceProperties).forEach((face) => {
const slotId = typeof mapping === 'string' ? mapping : mapping[face];
const faceMapping =
typeof mapping === 'string' ? mapping : mapping[face];
const slotId = typeof faceMapping === 'string' ? faceMapping : null;
const faceProperty = cubeFaceProperties[face];
const resourceName = objectContent[faceProperty];
if (!slotId || !resourceName) return;
const slot = theme.slots[slotId];
if (!slot || slot.kind !== 'texture') {
if (!slot || (slot.kind !== 'texture' && slot.kind !== 'material')) {
missingSlots.add(slotId);
return;
}
Expand All @@ -293,6 +398,14 @@ const applyThemeToStarter = (projectObject, starterThemeSlots, theme) => {
changed = true;
});
if (changed) changedObjectsCount++;
return;
}

if (
typeof mapping !== 'string' &&
themeCustomObjectProperties(objectContent, mapping)
) {
changedObjectsCount++;
}
}
);
Expand Down
Loading
Loading