Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions packages/dev/core/src/Materials/Textures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export * from "./rawTexture2DArray";
export * from "./rawTexture3D";
export * from "./refractionTexture";
export * from "./renderTargetTexture";
export * from "./textureMerger";
export * from "./textureSampler";
export * from "./texture";
export * from "./thinTexture";
Expand Down
275 changes: 275 additions & 0 deletions packages/dev/core/src/Materials/Textures/textureMerger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
import type { Scene } from "../../scene";
// import type { Nullable } from "../../types";
import type { IProceduralTextureCreationOptions } from "core/Materials/Textures/Procedurals/proceduralTexture";
import { ProceduralTexture } from "./Procedurals/proceduralTexture";
import type { BaseTexture } from "./baseTexture";
import type { TextureSize } from "./textureCreationOptions";
import { ShaderLanguage } from "core/Materials/shaderLanguage";
import { Logger } from "../../Misc/logger";
import { Constants } from "../../Engines/constants";
import type { Texture } from "./texture";

/**
* Configuration for a texture input source
*/
export interface ITextureChannelInput {
/** The texture to use as input */
texture: BaseTexture;
/** Source channel to read from (0=R, 1=G, 2=B, 3=A) */
sourceChannel: number;
}

/**
* Configuration for a constant value input source
*/
export interface IConstantChannelInput {
/** Constant value between 0.0 and 1.0 */
value: number;
}

/**
* Union type for channel input sources
*/
export type ChannelInput = ITextureChannelInput | IConstantChannelInput;

/**
* Configuration for texture merging operation
*/
export interface ITextureMergeConfiguration {
/** Configuration for red output channel */
red: ChannelInput;
/** Configuration for green output channel (optional, defaults to 0) */
green?: ChannelInput;
/** Configuration for blue output channel (optional, defaults to 0) */
blue?: ChannelInput;
/** Configuration for alpha output channel (optional, defaults to 1) */
alpha?: ChannelInput;
/** Output texture size. If not specified, uses the largest input texture size */
outputSize?: TextureSize;
/** Whether to generate mipmaps for the output texture */
generateMipMaps?: boolean;
}

/**
* Utility class for merging multiple textures into a single texture using ProceduralTexture
* Each output channel can be sourced from a texture channel or a constant value
*/
export class TextureMerger {
private static _ShaderName = "textureMerger";

/**
* Get the class name of the texture.
* @returns "ThinTexture"
*/
public getClassName(): string {
return "TextureMerger";
}

/**
* Check if a channel input is a texture input
* @param input The channel input to check
* @returns True if the input is a texture input, false otherwise
*/
private static _IsTextureInput(input: ChannelInput): input is ITextureChannelInput {
return "texture" in input;
}

/**
* Check if a channel input is a constant input
* @param input The channel input to check
* @returns True if the input is a constant input, false otherwise
*/
private static _IsConstantInput(input: ChannelInput): input is IConstantChannelInput {
return "value" in input;
}

private static _CopyTextureTransform(source: Texture, destination: Texture) {
destination.uOffset = source.uOffset;
destination.vOffset = source.vOffset;
destination.uScale = source.uScale;
destination.vScale = source.vScale;
destination.uAng = source.uAng;
destination.vAng = source.vAng;
destination.wAng = source.wAng;
destination.uRotationCenter = source.uRotationCenter;
destination.vRotationCenter = source.vRotationCenter;
}

/**
* Merge multiple texture channels into a single texture
* @param name Name for the resulting texture
* @param config Merge configuration
* @param scene Scene to create the texture in
* @returns The merged texture
*/
public static async MergeTexturesAsync(name: string, config: ITextureMergeConfiguration, scene: Scene): Promise<ProceduralTexture> {
Logger.Log(`TextureMerger.MergeTextures called with: ${name}`);
const channels = [config.red, config.green, config.blue, config.alpha];
const textureInputs: BaseTexture[] = [];
const textureInputMap: number[] = []; // Maps channel index to texture input index (-1 for constants)

// Collect unique textures and validate inputs
for (let channelIndex = 0; channelIndex < 4; channelIndex++) {
const channel = channels[channelIndex];
if (channel) {
if (this._IsTextureInput(channel)) {
// Validate source channel
if (channel.sourceChannel < 0 || channel.sourceChannel > 3) {
throw new Error("Source channel must be between 0 and 3 (R, G, B, A)");
}

// Find or add texture to inputs
let textureIndex = textureInputs.indexOf(channel.texture);
if (textureIndex === -1) {
textureIndex = textureInputs.length;
textureInputs.push(channel.texture);
}
textureInputMap[channelIndex] = textureIndex;
} else if (this._IsConstantInput(channel)) {
// Validate constant value
if (channel.value < 0 || channel.value > 1) {
throw new Error("Constant value must be between 0.0 and 1.0");
}
textureInputMap[channelIndex] = -1;
} else {
throw new Error("Invalid channel input configuration");
}
} else {
textureInputMap[channelIndex] = -1;
}
}

// Determine output size
let outputSize = config.outputSize;
if (!outputSize && textureInputs.length > 0) {
// Use the largest texture size
let maxSize = 0;
for (const texture of textureInputs) {
const size = texture.getSize();
const currentSize = Math.max(size.width, size.height);
if (currentSize > maxSize) {
maxSize = currentSize;
outputSize = size.width === size.height ? maxSize : size;
}
}
}
outputSize = outputSize || 512; // Fallback size

// Generate shader defines
const defines: string[] = [];
const usedTextures = new Set<number>();

for (let channelIndex = 0; channelIndex < 4; channelIndex++) {
const channel = channels[channelIndex];
const channelName = ["RED", "GREEN", "BLUE", "ALPHA"][channelIndex];

if (channel && this._IsTextureInput(channel)) {
defines.push(`${channelName}_FROM_TEXTURE`);
const textureIndex = textureInputMap[channelIndex];
usedTextures.add(textureIndex);
}
}

// Add texture defines for used textures
for (const textureIndex of usedTextures) {
defines.push(`USE_TEXTURE${textureIndex}`);
}

// Create the procedural texture
const outputTextureOptions: IProceduralTextureCreationOptions = {
type: Constants.TEXTURETYPE_HALF_FLOAT,
format: Constants.TEXTUREFORMAT_RGBA,
samplingMode: Constants.TEXTURE_NEAREST_SAMPLINGMODE,
generateDepthBuffer: false,
generateMipMaps: false,
shaderLanguage: scene.getEngine().isWebGPU ? ShaderLanguage.WGSL : ShaderLanguage.GLSL,
extraInitializationsAsync: async () => {
if (scene.getEngine().isWebGPU) {
await Promise.all([import("../../ShadersWGSL/textureMerger.fragment")]);
} else {
await Promise.all([import("../../Shaders/textureMerger.fragment")]);
}
},
};
const proceduralTexture = new ProceduralTexture(name, outputSize, this._ShaderName, scene, outputTextureOptions);
proceduralTexture.refreshRate = -1; // Do not auto-refresh

// Set the defines
proceduralTexture.defines = defines.length > 0 ? "#define " + defines.join("\n#define ") + "\n" : "";

// Set up texture inputs
for (let i = 0; i < textureInputs.length; i++) {
TextureMerger._CopyTextureTransform(textureInputs[i] as Texture, proceduralTexture);
proceduralTexture.setTexture(`inputTexture${i}`, textureInputs[i]);
}

// Set up channel configuration
for (let channelIndex = 0; channelIndex < 4; channelIndex++) {
const channel = channels[channelIndex];
const channelName = ["red", "green", "blue", "alpha"][channelIndex];

if (channel && this._IsTextureInput(channel)) {
const textureIndex = textureInputMap[channelIndex];
proceduralTexture.setInt(`${channelName}TextureIndex`, textureIndex);
proceduralTexture.setInt(`${channelName}SourceChannel`, channel.sourceChannel);
} else {
// Use constant value (either provided or default)
let constantValue: number;
if (channel && this._IsConstantInput(channel)) {
constantValue = channel.value;
} else {
// Use default values: 0 for RGB, 1 for alpha
constantValue = channelIndex === 3 ? 1.0 : 0.0;
}
proceduralTexture.setFloat(`${channelName}ConstantValue`, constantValue);
}
}

return await new Promise<ProceduralTexture>((resolve, reject) => {
// Compile and render
proceduralTexture.executeWhenReady(() => {
try {
proceduralTexture.render();
resolve(proceduralTexture);
} catch (error) {
reject(error instanceof Error ? error : new Error(String(error)));
}
});
});
}

/**
* Create a texture input configuration
* @param texture The texture to read from
* @param sourceChannel The channel to read (0=R, 1=G, 2=B, 3=A)
* @returns Texture channel input configuration
*/
public static CreateTextureInput(texture: BaseTexture, sourceChannel: number): ITextureChannelInput {
return { texture, sourceChannel };
}

/**
* Create a constant value input configuration
* @param value The constant value (0.0-1.0)
* @returns Constant channel input configuration
*/
public static CreateConstantInput(value: number): IConstantChannelInput {
return { value };
}

/**
* Create a simple RGBA channel packing configuration
* @param red Input for red channel
* @param green Input for green channel (optional, defaults to 0)
* @param blue Input for blue channel (optional, defaults to 0)
* @param alpha Input for alpha channel (optional, defaults to 1)
* @returns Texture merge configuration
*/
public static CreateRGBAConfiguration(red: ChannelInput, green?: ChannelInput, blue?: ChannelInput, alpha?: ChannelInput): ITextureMergeConfiguration {
return { red, green, blue, alpha };
}
}

// Debug: Check if class is properly exported
// Logger.Log("TextureMerger class defined:", typeof TextureMerger);
// Logger.Log("TextureMerger.MergeTextures:", typeof TextureMerger.MergeTextures);
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ vec2 geometry_tangent = vec2(1.0, 0.0);
#endif

#ifdef GEOMETRY_OPACITY
vec4 opacityFromTexture = texture2D(opacitySampler, vOpacityUV + uvOffset);
vec4 opacityFromTexture = texture2D(geometryOpacitySampler, vGeometryOpacityUV + uvOffset);
#endif

#ifdef DECAL
Expand Down
Loading
Loading