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
165 changes: 79 additions & 86 deletions package-lock.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { BoundProperty } from "../boundProperty";
import { Color3PropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/colorPropertyLine";
import { SyncedSliderPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/syncedSliderPropertyLine";
import { SwitchPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/switchPropertyLine";
import { BoundTextureProperty } from "../textures/boundTextureProperty";

/**
* Displays the lighting and color properties of a PBR material.
Expand All @@ -26,3 +27,31 @@ export const PBRMaterialLightingAndColorProperties: FunctionComponent<{ material
</>
);
};

/**
* Displays the texture channel properties of a PBR material.
* @param props - The required properties
* @returns A JSX element representing the texture channels.
*/
export const PBRMaterialTextureProperties: FunctionComponent<{ material: PBRMaterial }> = (props) => {
const { material } = props;
const scene = material.getScene();

return (
<>
<BoundTextureProperty label="Albedo" target={material} propertyKey="albedoTexture" scene={scene} />
<BoundTextureProperty label="Base Weight" target={material} propertyKey="baseWeightTexture" scene={scene} />
<BoundTextureProperty label="Base Diffuse Roughness" target={material} propertyKey="baseDiffuseRoughnessTexture" scene={scene} />
<BoundTextureProperty label="Metallic Roughness" target={material} propertyKey="metallicTexture" scene={scene} />
<BoundTextureProperty label="Reflection" target={material} propertyKey="reflectionTexture" scene={scene} cubeOnly />
<BoundTextureProperty label="Refraction" target={material} propertyKey="refractionTexture" scene={scene} />
<BoundTextureProperty label="Reflectivity" target={material} propertyKey="reflectivityTexture" scene={scene} />
<BoundTextureProperty label="Micro-surface" target={material} propertyKey="microSurfaceTexture" scene={scene} />
<BoundTextureProperty label="Bump" target={material} propertyKey="bumpTexture" scene={scene} />
<BoundTextureProperty label="Emissive" target={material} propertyKey="emissiveTexture" scene={scene} />
<BoundTextureProperty label="Opacity" target={material} propertyKey="opacityTexture" scene={scene} />
<BoundTextureProperty label="Ambient" target={material} propertyKey="ambientTexture" scene={scene} />
<BoundTextureProperty label="Lightmap" target={material} propertyKey="lightmapTexture" scene={scene} />
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,10 @@ import type { FunctionComponent } from "react";
import type { BaseTexture } from "core/index";
import type { DropdownOption } from "shared-ui-components/fluent/primitives/dropdown";

import { useCallback } from "react";

import { Constants } from "core/Engines/constants";
import { CubeTexture } from "core/Materials/Textures/cubeTexture";
import { Texture } from "core/Materials/Textures/texture";
import { ReadFile } from "core/Misc/fileTools";
import { ButtonLine } from "shared-ui-components/fluent/hoc/buttonLine";
import { FileUploadLine } from "shared-ui-components/fluent/hoc/fileUploadLine";
import { TextureUpload } from "shared-ui-components/fluent/hoc/textureUpload";
import { BooleanBadgePropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/booleanBadgePropertyLine";
import { NumberDropdownPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/dropdownPropertyLine";
import { TextInputPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/inputPropertyLine";
Expand All @@ -26,56 +22,10 @@ import { TexturePreview } from "./texturePreview";
export const BaseTexturePreviewProperties: FunctionComponent<{ texture: BaseTexture }> = (props) => {
const { texture } = props;

const isUpdatable = texture instanceof Texture || texture instanceof CubeTexture;

const updateTexture = useCallback(
(file: File) => {
ReadFile(
file,
(data) => {
const blob = new Blob([data], { type: "octet/stream" });

const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = () => {
const base64data = reader.result as string;

if (texture instanceof CubeTexture) {
let extension: string | undefined = undefined;
if (file.name.toLowerCase().indexOf(".dds") > 0) {
extension = ".dds";
} else if (file.name.toLowerCase().indexOf(".env") > 0) {
extension = ".env";
}

texture.updateURL(base64data, extension);
} else if (texture instanceof Texture) {
texture.updateURL(base64data);
}
};
},
undefined,
true
);
},
[texture]
);

return (
<>
<TexturePreview texture={texture} width={256} height={256} />
{/* TODO: This should probably be dynamically fetching a list of supported texture extensions. */}
{isUpdatable && (
<FileUploadLine
label="Load Texture From File"
accept=".jpg, .png, .tga, .dds, .env, .exr"
onClick={(files) => {
if (files.length > 0) {
updateTexture(files[0]);
}
}}
/>
)}
<TextureUpload texture={texture} />
<ButtonLine label="Edit Texture (coming soon!)" onClick={() => {}} />
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ChooseTexturePropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/chooseTexturePropertyLine";
import { useProperty } from "../../../hooks/compoundPropertyHooks";
import { usePropertyChangedNotifier } from "../../../contexts/propertyContext";
import type { Scene } from "core/scene";
import type { BaseTexture } from "core/Materials/Textures/baseTexture";
import type { Nullable } from "core/types";

/**
* Type alias for objects with texture properties
*/
type TextureHolder<K extends string> = Record<K, Nullable<BaseTexture>>;

/**
* Props for BoundTextureProperty
*/
type BoundTexturePropertyProps<K extends string> = {
label: string;
target: TextureHolder<K>;
propertyKey: K;
scene: Scene;
cubeOnly?: boolean;
};

/**
* Helper to bind texture properties without needing defaultValue
* @param props - The required properties
* @returns ChooseTexturePropertyLine component
*/
export function BoundTextureProperty<K extends string>(props: BoundTexturePropertyProps<K>) {
const { label, target, propertyKey, scene, cubeOnly } = props;
const value = useProperty(target, propertyKey);
const notifyPropertyChanged = usePropertyChangedNotifier();

return (
<ChooseTexturePropertyLine
label={label}
value={value}
onChange={(texture) => {
const oldValue = target[propertyKey];
target[propertyKey] = texture;
notifyPropertyChanged(target, propertyKey, oldValue, texture);
}}
scene={scene}
cubeOnly={cubeOnly}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
PBRBaseMaterialIridescenceProperties,
PBRBaseMaterialSheenProperties,
} from "../../../components/properties/materials/pbrBaseMaterialProperties";
import { PBRMaterialLightingAndColorProperties } from "../../../components/properties/materials/pbrMaterialProperties";
import { PBRMaterialLightingAndColorProperties, PBRMaterialTextureProperties } from "../../../components/properties/materials/pbrMaterialProperties";
import {
OpenPBRMaterialBaseProperties,
OpenPBRMaterialCoatProperties,
Expand Down Expand Up @@ -114,6 +114,10 @@ export const MaterialPropertiesServiceDefinition: ServiceDefinition<[], [IProper
key: "PBR Material Properties",
predicate: (entity: unknown) => entity instanceof PBRMaterial,
content: [
{
section: "Textures",
component: ({ context }) => <PBRMaterialTextureProperties material={context} />,
},
{
section: "Lighting & Colors",
component: ({ context }) => <PBRMaterialLightingAndColorProperties material={context} />,
Expand Down
34 changes: 12 additions & 22 deletions packages/dev/sharedUiComponents/src/fluent/hoc/fileUploadLine.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,25 @@
import { useRef } from "react";
import type { FunctionComponent } from "react";
import { ButtonLine } from "./buttonLine";
import { LineContainer } from "./propertyLines/propertyLine";
import { UploadButton } from "../primitives/uploadButton";
import type { ButtonProps } from "../primitives/button";
import { ArrowUploadRegular } from "@fluentui/react-icons";

type FileUploadLineProps = Omit<ButtonProps, "onClick" | "label"> & {
onClick: (files: FileList) => void;
label: string; // Require a label when button is the entire line (by default, label is optional on a button)
label: string; // Require a label when button is the entire line (by default, label is optional on an UploadButton
accept: string;
};

export const FileUploadLine: FunctionComponent<FileUploadLineProps> = (props) => {
/**
* A full-width line with an upload button.
* For just the button without the line wrapper, use UploadButton directly.
* @returns An UploadButton wrapped in a LineContainer
*/
export const FileUploadLine: FunctionComponent<FileUploadLineProps> = ({ onClick, label, accept, ...buttonProps }) => {
FileUploadLine.displayName = "FileUploadLine";
const inputRef = useRef<HTMLInputElement>(null);

const handleButtonClick = () => {
inputRef.current?.click();
};

const handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
const files = evt.target.files;
if (files && files.length) {
props.onClick(files);
}
evt.target.value = "";
};

return (
<>
<ButtonLine onClick={handleButtonClick} icon={ArrowUploadRegular} label={props.label}></ButtonLine>
<input ref={inputRef} type="file" accept={props.accept} style={{ display: "none" }} onChange={handleChange} />
</>
<LineContainer>
<UploadButton onUpload={onClick} accept={accept} label={label} {...buttonProps} />
</LineContainer>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { FunctionComponent } from "react";
import type { BaseTexture } from "core/Materials/Textures/baseTexture";
import type { Nullable } from "core/types";
import type { PropertyLineProps } from "./propertyLine";
import type { ChooseTextureProps } from "../../primitives/chooseTexture";

import { PropertyLine } from "./propertyLine";
import { ChooseTexture } from "../../primitives/chooseTexture";

type ChooseTexturePropertyLineProps = PropertyLineProps<Nullable<BaseTexture>> & ChooseTextureProps;

/**
* A property line with a ComboBox for selecting from existing scene textures
* and a button for uploading new texture files.
* @param props - ChooseTextureProps & PropertyLineProps
* @returns property-line wrapped ChooseTexture component
*/
export const ChooseTexturePropertyLine: FunctionComponent<ChooseTexturePropertyLineProps> = (props) => {
ChooseTexturePropertyLine.displayName = "ChooseTexturePropertyLine";

return (
<PropertyLine {...props}>
<ChooseTexture {...props} />
</PropertyLine>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { FunctionComponent } from "react";
import type { PropertyLineProps } from "./propertyLine";
import type { ComboBoxProps } from "../../primitives/comboBox";

import { PropertyLine } from "./propertyLine";
import { ComboBox } from "../../primitives/comboBox";

type ComboBoxPropertyLineProps = ComboBoxProps & PropertyLineProps<string>;

/**
* A property line with a filterable ComboBox
* @param props - ComboBoxProps & PropertyLineProps
* @returns property-line wrapped ComboBox component
*/
export const ComboBoxPropertyLine: FunctionComponent<ComboBoxPropertyLineProps> = (props) => {
ComboBoxPropertyLine.displayName = "ComboBoxPropertyLine";

return (
<PropertyLine {...props}>
<ComboBox {...props} />
</PropertyLine>
);
};
109 changes: 109 additions & 0 deletions packages/dev/sharedUiComponents/src/fluent/hoc/textureUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import type { FunctionComponent } from "react";
import { useCallback } from "react";
import type { BaseTexture } from "core/Materials/Textures/baseTexture";
import type { Scene } from "core/scene";
import { Texture } from "core/Materials/Textures/texture";
import { CubeTexture } from "core/Materials/Textures/cubeTexture";
import { ReadFile } from "core/Misc/fileTools";
import { UploadButton } from "../primitives/uploadButton";

type TextureUploadUpdateProps = {
/**
* Existing texture to update via updateURL
*/
texture: BaseTexture;
/**
* Callback after texture is updated
*/
onChange?: (texture: BaseTexture) => void;
scene?: never;
cubeOnly?: never;
};

type TextureUploadCreateProps = {
/**
* The scene to create the texture in
*/
scene: Scene;
/**
* Callback when a new texture is created
*/
onChange: (texture: BaseTexture) => void;
/**
* Whether to create cube textures
*/
cubeOnly?: boolean;
texture?: never;
};

type TextureUploadProps = TextureUploadUpdateProps | TextureUploadCreateProps;

/**
* A button that uploads a file and either:
* - Updates an existing Texture or CubeTexture via updateURL (if texture prop is provided)
* - Creates a new Texture or CubeTexture (if scene/onChange props are provided)
* @param props TextureUploadProps
* @returns UploadButton component that handles texture upload
*/
export const TextureUpload: FunctionComponent<TextureUploadProps> = (props) => {
TextureUpload.displayName = "TextureUpload";
const label = props.texture ? "Upload Texture" : undefined;
// TODO: This should probably be dynamically fetching a list of supported texture extensions
const accept = ".jpg, .png, .tga, .dds, .env, .exr";
const handleUpload = useCallback(
(files: FileList) => {
const file = files[0];
if (!file) {
return;
}

ReadFile(
file,
(data) => {
const blob = new Blob([data], { type: "octet/stream" });

// Update existing texture
if (props.texture) {
const { texture, onChange } = props;
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = () => {
const base64data = reader.result as string;

if (texture instanceof CubeTexture) {
let extension: string | undefined = undefined;
if (file.name.toLowerCase().indexOf(".dds") > 0) {
extension = ".dds";
} else if (file.name.toLowerCase().indexOf(".env") > 0) {
extension = ".env";
}
texture.updateURL(base64data, extension, () => onChange?.(texture));
} else if (texture instanceof Texture) {
texture.updateURL(base64data, null, () => onChange?.(texture));
}
};
} else {
// Create new texture
const { scene, cubeOnly, onChange } = props;
const url = URL.createObjectURL(blob);
const extension = file.name.split(".").pop()?.toLowerCase();

// Revoke the object URL after texture loads to prevent memory leak
const revokeUrl = () => URL.revokeObjectURL(url);

const newTexture = cubeOnly
? new CubeTexture(url, scene, [], false, undefined, revokeUrl, undefined, undefined, false, extension ? "." + extension : undefined)
: new Texture(url, scene, false, false, undefined, revokeUrl);

onChange(newTexture);
}
},
undefined,
true
);
},
[props]
);

return <UploadButton onUpload={handleUpload} accept={accept} title={"Upload Texture"} label={label} />;
};
Loading