-
Notifications
You must be signed in to change notification settings - Fork 3.6k
[InspectorV2] Texture preview component #17236
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
georginahalpern
merged 11 commits into
BabylonJS:master
from
georginahalpern:texturePreview
Oct 6, 2025
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
5e86c52
texture previewer
e49263d
Merge branch 'master' of https://github.com/BabylonJS/Babylon.js into…
681abf8
Merge branch 'master' of https://github.com/BabylonJS/Babylon.js into…
c5873cd
Merge branch 'master' of https://github.com/BabylonJS/Babylon.js into…
ac173b8
size not being updated because promise never resolving
05677ab
Merge branch 'master' of https://github.com/BabylonJS/Babylon.js into…
613e293
working texture preview
925e5cf
Merge branch 'master' of https://github.com/BabylonJS/Babylon.js into…
4b0d9e7
Update packages/dev/inspector-v2/src/components/properties/textures/t…
georginahalpern e411ec8
Update packages/dev/inspector-v2/src/components/properties/textures/t…
georginahalpern 7ce6e6a
pr cmments
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
262 changes: 262 additions & 0 deletions
262
packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; | ||
georginahalpern marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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 | ||
georginahalpern marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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)}> | ||
georginahalpern marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{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; | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.