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
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import { FileUploadLine } from "shared-ui-components/fluent/hoc/fileUploadLine";
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";
import { PlaceholderPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/propertyLine";
import { StringifiedPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/stringifiedPropertyLine";
import { SwitchPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/switchPropertyLine";
import { SyncedSliderPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/syncedSliderPropertyLine";
import { TextPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/textPropertyLine";
import { useProperty } from "../../../hooks/compoundPropertyHooks";
import { BoundProperty } from "../boundProperty";
import { FindTextureFormat, FindTextureType } from "./textureFormatUtils";
import { TexturePreview } from "./texturePreview";

export const BaseTexturePreviewProperties: FunctionComponent<{ texture: BaseTexture }> = (props) => {
const { texture } = props;
Expand Down Expand Up @@ -63,7 +63,7 @@ export const BaseTexturePreviewProperties: FunctionComponent<{ texture: BaseText

return (
<>
<PlaceholderPropertyLine label="TODO: Texture Preview" value={null} onChange={() => {}} />
<TexturePreview texture={texture} width={256} height={256} />
{/* TODO: This should probably be dynamically fetching a list of supported texture extensions. */}
{isUpdatable && (
<FileUploadLine
Expand All @@ -76,7 +76,7 @@ export const BaseTexturePreviewProperties: FunctionComponent<{ texture: BaseText
}}
/>
)}
<PlaceholderPropertyLine label="TODO:Texture Editor" value={null} onChange={() => {}} />
<ButtonLine label="Edit Texture (coming soon!)" onClick={() => {}} />
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
import type { BaseTexture } from "core/Materials/Textures/baseTexture";
import { Button, Toolbar, ToolbarButton, makeStyles } from "@fluentui/react-components";
import { useRef, useState, useEffect, useCallback } from "react";
import type { FunctionComponent } from "react";
import { GetTextureDataAsync, WhenTextureReadyAsync } from "core/Misc/textureTools";
import { useProperty } from "../../../hooks/compoundPropertyHooks";
import type { Texture } from "core/Materials";

const useStyles = makeStyles({
root: { display: "flex", flexDirection: "column", gap: "8px" },
controls: {
display: "flex",
gap: "2px",
padding: "2px",
width: "100%",
justifyContent: "center",
},
controlButton: {
minWidth: "auto",
flex: "1 1 0", // Equal flex grow/shrink with 0 basis
paddingVertical: "4px",
paddingHorizontal: "8px",
overflow: "hidden",
textOverflow: "ellipsis",
},
preview: {
border: "1px solid #ccc",
marginTop: "8px",
maxWidth: "100%",
marginLeft: "auto",
marginRight: "auto",
display: "block",
},
});

// This method of holding TextureChannels was brought over from inspectorv1 and can likely be refactored/simplified
const TextureChannelStates = {
R: { R: true, G: false, B: false, A: false },
G: { R: false, G: true, B: false, A: false },
B: { R: false, G: false, B: true, A: false },
A: { R: false, G: false, B: false, A: true },
ALL: { R: true, G: true, B: true, A: true },
};

type TexturePreviewProps = {
texture: BaseTexture;
width: number;
height: number;
};

export const TexturePreview: FunctionComponent<TexturePreviewProps> = (props) => {
const { texture, width, height } = props;
const classes = useStyles();
const canvasRef = useRef<HTMLCanvasElement>(null);
const [channels, setChannels] = useState(TextureChannelStates.ALL);
const [face, setFace] = useState(0);
const internalTexture = useProperty(texture, "_texture");

const updatePreviewCanvasSize = useCallback(
(previewCanvas: HTMLCanvasElement) => {
// This logic was brought over from inspectorv1 and can likely be refactored/simplified
const size = texture.getSize();
const ratio = size.width / size.height;
let w = width;
let h = (w / ratio) | 1;
const engine = texture.getScene()?.getEngine();

if (engine && h > engine.getCaps().maxTextureSize) {
w = size.width;
h = size.height;
}

previewCanvas.width = w;
previewCanvas.height = h;
previewCanvas.style.width = w + "px";
previewCanvas.style.height = h + "px";

return {
w: w,
h: h,
};
},
[canvasRef.current, texture, width, height, internalTexture]
);

const updatePreviewAsync = useCallback(async () => {
const previewCanvas = canvasRef.current;
if (!previewCanvas) {
return;
}
try {
await WhenTextureReadyAsync(texture); // Ensure texture is loaded before grabbing size
const { w, h } = updatePreviewCanvasSize(previewCanvas); // Grab desired size
const data = await ApplyChannelsToTextureDataAsync(texture, w, h, face, channels); // get channel data to load onto canvas context
const context = previewCanvas.getContext("2d");
if (context) {
const imageData = context.createImageData(w, h);
imageData.data.set(data);
context.putImageData(imageData, 0, 0);
}
} catch {
updatePreviewCanvasSize(previewCanvas); // If we fail above, best effort sizing preview canvas
}
}, [[texture, width, height, face, channels, internalTexture]]);

useEffect(() => {
void updatePreviewAsync();
}, [texture, width, height, face, channels, internalTexture]);

return (
<div className={classes.root}>
{texture.isCube ? (
<Toolbar className={classes.controls} aria-label="Cube Faces">
{["+X", "-X", "+Y", "-Y", "+Z", "-Z"].map((label, idx) => (
<ToolbarButton className={classes.controlButton} key={label} appearance={face === idx ? "primary" : "subtle"} onClick={() => setFace(idx)}>
{label}
</ToolbarButton>
))}
</Toolbar>
) : (
<Toolbar className={classes.controls} aria-label="Channels">
{(["R", "G", "B", "A", "ALL"] as const).map((ch) => (
<ToolbarButton
className={classes.controlButton}
key={ch}
appearance={channels === TextureChannelStates[ch] ? "primary" : "subtle"}
onClick={() => setChannels(TextureChannelStates[ch])}
>
{ch}
</ToolbarButton>
))}
</Toolbar>
)}
<canvas ref={canvasRef} className={classes.preview} />
{texture.isRenderTarget && (
<Button
appearance="outline"
onClick={() => {
void updatePreviewAsync();
}}
>
Refresh
</Button>
)}
</div>
);
};

/**
* Defines which channels of the texture to retrieve with {@link TextureHelper.GetTextureDataAsync}.
*/
type TextureChannelsToDisplay = {
/**
* True if the red channel should be included.
*/
R: boolean;
/**
* True if the green channel should be included.
*/
G: boolean;
/**
* True if the blue channel should be included.
*/
B: boolean;
/**
* True if the alpha channel should be included.
*/
A: boolean;
};

/**
* Gets the data of the specified texture by rendering it to an intermediate RGBA texture and retrieving the bytes from it.
* This is convienent to get 8-bit RGBA values for a texture in a GPU compressed format.
* @param texture the source texture
* @param width the width of the result, which does not have to match the source texture width
* @param height the height of the result, which does not have to match the source texture height
* @param face if the texture has multiple faces, the face index to use for the source
* @param channels a filter for which of the RGBA channels to return in the result
* @param lod if the texture has multiple LODs, the lod index to use for the source
* @returns the 8-bit texture data
*/
async function ApplyChannelsToTextureDataAsync(
texture: BaseTexture,
width: number,
height: number,
face: number,
channels: TextureChannelsToDisplay,
lod: number = 0
): Promise<Uint8Array> {
const data = await GetTextureDataAsync(texture, width, height, face, lod);

if (!channels.R || !channels.G || !channels.B || !channels.A) {
for (let i = 0; i < width * height * 4; i += 4) {
// If alpha is the only channel, just display alpha across all channels
if (channels.A && !channels.R && !channels.G && !channels.B) {
data[i] = data[i + 3];
data[i + 1] = data[i + 3];
data[i + 2] = data[i + 3];
data[i + 3] = 255;
continue;
}
let r = data[i],
g = data[i + 1],
b = data[i + 2],
a = data[i + 3];
// If alpha is not visible, make everything 100% alpha
if (!channels.A) {
a = 255;
}
// If only one color channel is selected, map both colors to it. If two are selected, the unused one gets set to 0
if (!channels.R) {
if (channels.G && !channels.B) {
r = g;
} else if (channels.B && !channels.G) {
r = b;
} else {
r = 0;
}
}
if (!channels.G) {
if (channels.R && !channels.B) {
g = r;
} else if (channels.B && !channels.R) {
g = b;
} else {
g = 0;
}
}
if (!channels.B) {
if (channels.R && !channels.G) {
b = r;
} else if (channels.G && !channels.R) {
b = g;
} else {
b = 0;
}
}
data[i] = r;
data[i + 1] = g;
data[i + 2] = b;
data[i + 3] = a;
}
}

//To flip image on Y axis.
if ((texture as Texture).invertY || texture.isCube) {
const numberOfChannelsByLine = width * 4;
const halfHeight = height / 2;
for (let i = 0; i < halfHeight; i++) {
for (let j = 0; j < numberOfChannelsByLine; j++) {
const currentCell = j + i * numberOfChannelsByLine;
const targetLine = height - i - 1;
const targetCell = j + targetLine * numberOfChannelsByLine;

const temp = data[currentCell];
data[currentCell] = data[targetCell];
data[targetCell] = temp;
}
}
}
return data;
}