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
93 changes: 83 additions & 10 deletions api/material.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@ export function setFlockReference(ref) {
flock = ref;
}

// Gradient direction follows CSS linear-gradient: 0 is bottom to top, increasing
// clockwise, rotating within the object's local XY plane.
function gradientAxisFor(direction) {
const radians = ((Number(direction) || 0) * Math.PI) / 180;
return { x: Math.sin(radians), y: Math.cos(radians) };
}

// The angle is part of a gradient's identity, so it has to reach the material cache key.
function withGradientDirection(colorKey, direction) {
return Number.isFinite(direction) ? `${colorKey}@${direction}` : colorKey;
}

function readGradientDirection(value) {
if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;
return Number.isFinite(value.direction) ? value.direction : undefined;
}

export const flockMaterial = {
adjustMaterialTilingToMesh(mesh, material, _unitsPerTile = null) {
return; // Don't scale textures - need to change the mesh UVs instead
Expand Down Expand Up @@ -47,6 +64,17 @@ export const flockMaterial = {
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
},
getColorFromString(colourString) {
// A colour list or a material/gradient descriptor can reach solid-colour APIs
// such as tint and highlight; fall back to the first colour rather than black.
if (Array.isArray(colourString)) {
return flock.getColorFromString(colourString[0]);
}

if (typeof colourString === 'object' && colourString !== null) {
const inner = colourString.color ?? colourString.baseColor;
return inner === undefined ? '#000000' : flock.getColorFromString(inner);
}

if (typeof colourString !== 'string') {
return '#000000';
}
Expand Down Expand Up @@ -193,11 +221,13 @@ export const flockMaterial = {
const parts = mat.metadata.cacheKey.split('_');
const lastPart = parts[parts.length - 1];
const hasGlowPart = lastPart === 'glow' || lastPart === 'noglow';
const colorPart = parts[1];
const [colorPart, directionPart] = parts[1].split('@');
const color = colorPart.includes('-') ? colorPart.split('-') : colorPart;
const parsedDirection = parseFloat(directionPart);
const parsedAlpha = parseFloat(parts[2]);
return {
color,
...(Number.isFinite(parsedDirection) ? { direction: parsedDirection } : {}),
materialName:
mat.metadata.texName ||
parts.slice(3, hasGlowPart ? -1 : parts.length).join('_') ||
Expand Down Expand Up @@ -507,6 +537,14 @@ export const flockMaterial = {
return;
}

// A gradient is one material spanning the object, not a colour per sub-mesh,
// so it goes straight to the material pipeline.
if (readGradientDirection(color) !== undefined) {
flock.applyMaterialToHierarchy(mesh, color, { applyColor: true });
if (mesh.metadata?.glow) flock.glowMesh(mesh);
return;
}

const getPartNameFromMesh = flock.getCanonicalPartName;

const getRootMesh = (node) => {
Expand Down Expand Up @@ -878,7 +916,7 @@ export const flockMaterial = {
});
});
},
createMaterial({ color, materialName, alpha, glow = false } = {}) {
createMaterial({ color, materialName, alpha, glow = false, direction } = {}) {
if (flock?.materialsDebug) console.log(`Create material: ${materialName}`);
let material;
const texturePath = flock.texturePath + materialName;
Expand All @@ -896,7 +934,12 @@ export const flockMaterial = {
if (Array.isArray(color) && color.length >= 2) {
// Use gradient for Flat material
if (materialName === 'none.png') {
if (color.length === 2) {
if (Number.isFinite(direction)) {
// An angled gradient needs the shader path even for two colours;
// GradientMaterial only runs bottom to top.
material = flock.createMultiColorGradientMaterial(materialName, color, direction);
material.backFaceCulling = false;
} else if (color.length === 2) {
material = new flock.GradientMaterial(materialName, flock.scene);
material.bottomColor = flock.BABYLON.Color3.FromHexString(
flock.getColorFromString(color[0])
Expand Down Expand Up @@ -961,7 +1004,7 @@ export const flockMaterial = {
if (flock.materialsDebug) console.log(`Created the material: ${material.name}`);
return material;
},
createMultiColorGradientMaterial(name, colors) {
createMultiColorGradientMaterial(name, colors, direction = 0) {
if (!flock.BABYLON.Effect.ShadersStore['multiColorGradientFogVertexShader']) {
flock.BABYLON.Effect.ShadersStore['multiColorGradientFogVertexShader'] = `
precision highp float;
Expand All @@ -970,14 +1013,15 @@ export const flockMaterial = {
uniform mat4 world;
uniform mat4 view;
uniform vec2 minMax;
uniform vec2 gradientAxis;
varying float vGradient;
varying vec3 vFogPosition;

void main(void) {
vec4 worldPosition = world * vec4(position, 1.0);
vec4 viewPosition = view * worldPosition;
gl_Position = worldViewProjection * vec4(position, 1.0);
vGradient = (position.y - minMax.x) / (minMax.y - minMax.x);
vGradient = (dot(position.xy, gradientAxis) - minMax.x) / max(1e-5, minMax.y - minMax.x);
vFogPosition = viewPosition.xyz;
}
`;
Expand Down Expand Up @@ -1050,6 +1094,7 @@ export const flockMaterial = {
'colors',
'alpha',
'minMax',
'gradientAxis',
'fogColor',
'fogDensity',
'fogStart',
Expand All @@ -1074,10 +1119,18 @@ export const flockMaterial = {
console.log('Color array:', color3Array);
}

const axis = gradientAxisFor(direction);

shaderMaterial.setInt('colorCount', Math.min(colors.length, 16));
shaderMaterial.setArray3('colors', color3Array);
shaderMaterial.setFloat('alpha', 1.0);
shaderMaterial.setVector2('minMax', new flock.BABYLON.Vector2(-1, 1));
shaderMaterial.setVector2('gradientAxis', new flock.BABYLON.Vector2(axis.x, axis.y));

shaderMaterial.metadata = {
...(shaderMaterial.metadata || {}),
gradientDirection: Number(direction) || 0,
};

flock.registerFogAwareShaderMaterial(shaderMaterial);
flock.updateFogUniformsForShaderMaterial(shaderMaterial);
Expand Down Expand Up @@ -1436,6 +1489,7 @@ export const flockMaterial = {
let texName = 'none.png';
let finalAlpha = alpha;
let finalGlow = false;
let finalDirection;

if (isObject) {
const inner = colorInput.color || colorInput.baseColor;
Expand All @@ -1456,17 +1510,22 @@ export const flockMaterial = {
? colorInput.alpha
: alpha;
finalGlow = inner.glow !== undefined ? inner.glow : (colorInput.glow ?? false);
finalDirection = readGradientDirection(inner) ?? readGradientDirection(colorInput);
} else {
rawColor = inner || '#ffffff';
texName = colorInput.materialName || colorInput.textureSet || 'none.png';
finalAlpha = colorInput.alpha !== undefined ? colorInput.alpha : alpha;
finalGlow = colorInput.glow ?? false;
finalDirection = readGradientDirection(colorInput);
}
} else {
rawColor = colorInput || '#ffffff';
}

const colorKey = Array.isArray(rawColor) ? rawColor.join('-') : rawColor;
const colorKey = withGradientDirection(
Array.isArray(rawColor) ? rawColor.join('-') : rawColor,
finalDirection
);
const alphaKey = parseFloat(finalAlpha).toFixed(2);
const glowKey = finalGlow ? 'glow' : 'noglow';
const cacheKey = `mat_${colorKey}_${alphaKey}_${texName}_${glowKey}`.toLowerCase();
Expand All @@ -1479,6 +1538,7 @@ export const flockMaterial = {
materialName: texName,
alpha: finalAlpha,
glow: finalGlow,
...(Number.isFinite(finalDirection) ? { direction: finalDirection } : {}),
};

const newMat = flock.createMaterial(materialParams);
Expand Down Expand Up @@ -1536,9 +1596,12 @@ export const flockMaterial = {

const makeTargetCacheKey = (v) => {
const rawColor = getRawColor(v);
const colorKey = Array.isArray(rawColor)
? rawColor.join('-')
: flock.getColorFromString(rawColor) || '#ffffff';
const colorKey = withGradientDirection(
Array.isArray(rawColor)
? rawColor.join('-')
: flock.getColorFromString(rawColor) || '#ffffff',
readGradientDirection(v)
);
const texName = String(getTexName(v));
const alphaKey = parseFloat(getAlpha(v)).toFixed(2);
const glow =
Expand Down Expand Up @@ -1572,7 +1635,17 @@ export const flockMaterial = {
const mat = m.material;
mat.metadata._minMaxObserver = mat.onBindObservable.add((boundMesh) => {
const bb = boundMesh.getBoundingInfo().boundingBox;
mat.setVector2('minMax', new flock.BABYLON.Vector2(bb.minimum.y, bb.maximum.y));
const axis = gradientAxisFor(mat.metadata?.gradientDirection ?? 0);
let low = Infinity;
let high = -Infinity;
for (const x of [bb.minimum.x, bb.maximum.x]) {
for (const y of [bb.minimum.y, bb.maximum.y]) {
const projected = x * axis.x + y * axis.y;
low = Math.min(low, projected);
high = Math.max(high, projected);
}
}
mat.setVector2('minMax', new flock.BABYLON.Vector2(low, high));
});
}
}
Expand Down
20 changes: 16 additions & 4 deletions api/scene.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,25 @@ export const flockScene = {

let color = input;

// The gradient colour block hands over a descriptor. Unwrap to its colours
// before the material conversion below, so the sky builds its own gradient
// and sizes minMax to the sky sphere; the sky only does bottom to top, so
// the direction is ignored.
if (color && typeof color === 'object' && !Array.isArray(color) && Array.isArray(color.color)) {
// Only for an untextured descriptor: with a texture the colour list means
// palette replacement, which the material conversion has to handle.
const texName = color.materialName || color.textureSet || 'none.png';
if (texName === 'none.png') color = color.color;
}

// Convert object input to a material (handles texture + colors)
if (
typeof input === 'object' &&
!(input instanceof flock.BABYLON.Material) &&
!Array.isArray(input)
color &&
typeof color === 'object' &&
!(color instanceof flock.BABYLON.Material) &&
!Array.isArray(color)
) {
color = flock.createMaterial(input);
color = flock.createMaterial(color);
}

if (!color) return;
Expand Down
36 changes: 34 additions & 2 deletions blocks/materials.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function defineMaterialsBlocks() {
{
type: 'input_value',
name: 'COLOR',
check: ['Colour', 'Array'], // Accepts either Colour or Array
check: ['Colour', 'Array', 'Gradient'], // Accepts a Colour, an Array or a Gradient
},
],
inputsInline: true,
Expand Down Expand Up @@ -688,6 +688,8 @@ export function defineMaterialsBlocks() {
type: 'input_value',
name: 'BASE_COLOR',
colour: '#ffffff', // Default to white
// No Gradient: a texture and a gradient can't share one material.
check: ['Colour', 'Array'],
},
{
type: 'input_value',
Expand Down Expand Up @@ -718,7 +720,7 @@ export function defineMaterialsBlocks() {
type: 'input_value',
name: 'COLOR',
colour: '#6495ED',
check: ['Colour', 'Array'],
check: ['Colour', 'Array', 'Gradient'],
},
{
type: 'input_value',
Expand All @@ -739,6 +741,36 @@ export function defineMaterialsBlocks() {
},
};

Blockly.Blocks['gradient_colour'] = {
init: function () {
this.jsonInit({
type: 'gradient_colour',
message0: translate('gradient_colour'),
args0: [
{
type: 'input_value',
name: 'COLORS',
check: 'Array',
},
{
type: 'field_number',
name: 'DIRECTION',
value: 0,
min: 0,
max: 360,
precision: 1,
},
],
output: ['Gradient', 'Material'],
inputsInline: true,
colour: categoryColours['Materials'],
tooltip: getTooltip('gradient_colour'),
});
this.setHelpUrl(getHelpUrlFor(this.type));
this.setStyle('materials_blocks');
},
};

function attachSetMaterialOnChange(block) {
const ws = block.workspace;
const touches = makeTouchesInputSubtree(block, ws, 'MATERIAL');
Expand Down
11 changes: 11 additions & 0 deletions generators/generators-material.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,15 @@ export function registerMaterialGenerators(javascriptGenerator) {
const code = `{ color: ${color}, materialName: "none.png", alpha: ${alpha} }`;
return [code, javascriptGenerator.ORDER_ATOMIC];
};

// Gradient colour ----------------------------------------------
javascriptGenerator.forBlock['gradient_colour'] = function (block) {
const colors =
javascriptGenerator.valueToCode(block, 'COLORS', javascriptGenerator.ORDER_ATOMIC) || '[]';
const direction = Number(block.getFieldValue('DIRECTION')) || 0;

// No alpha key, so the surrounding block's alpha still applies.
const code = `{ color: ${colors}, materialName: "none.png", direction: ${direction} }`;
return [code, javascriptGenerator.ORDER_ATOMIC];
};
}
3 changes: 3 additions & 0 deletions locale/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ export default {
random_colour: 'random color',
material: 'material %1 %2 alpha %3',
gradient_material: 'material %1 alpha %2',
gradient_colour: 'gradient %1 direction %2°',
set_material: 'set material of %1 to %2',

// Custom block translations - Physics blocks
Expand Down Expand Up @@ -538,6 +539,8 @@ export default {
random_colour_tooltip: 'Generate a random color.\nKeyword: randcol',
material_tooltip: 'Define material properties',
gradient_material_tooltip: 'Define material properties',
gradient_colour_tooltip:
'Blend between two or more colors. Direction is in degrees: 0 is bottom to top, 90 is left to right.\nKeyword: gradient',
set_material_tooltip: 'Set the specified material on the given object.',

// Tooltip translations - Physics blocks
Expand Down
3 changes: 3 additions & 0 deletions locale/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ export default {
random_colour: 'color aleatorio', // human
material: 'material %1 %2 alfa %3', // human
gradient_material: 'material %1 alfa %2', // human
gradient_colour: 'degradado %1 dirección %2°', // ai
set_material: 'establecer material de %1 a %2', // human

// Custom block translations - Physics blocks
Expand Down Expand Up @@ -539,6 +540,8 @@ export default {
random_colour_tooltip: 'Genera un color aleatorio.\nPalabra clave: color aleatorio', // human
material_tooltip: 'Define propiedades del material', // human
gradient_material_tooltip: 'Define propiedades del material (gradiente)', // human
gradient_colour_tooltip:
'Mezcla dos o más colores. La dirección se indica en grados: 0 es de abajo a arriba, 90 es de izquierda a derecha.\nPalabra clave: degradado', // ai
set_material_tooltip: 'Establecer el material especificado al objeto indicado.', // human

// Tooltip translations - Physics blocks
Expand Down
Loading
Loading