瀏覽代碼

Merge pull request #8752 from DarraghBurkeMS/selection

Texture Inspector: Selection, Channel Locking, Mip Previews
David Catuhe 5 年之前
父節點
當前提交
4e34bb49f7
共有 18 個文件被更改,包括 705 次插入270 次删除
  1. 1 1
      dist/preview release/what's new.md
  2. 4 1
      inspector/src/components/actionTabs/actionTabs.scss
  3. 1 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/texturePropertyGridComponent.tsx
  4. 3 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/bottomBar.tsx
  5. 10 2
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/contrast.ts
  6. 2 1
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/defaultTools.ts
  7. 13 13
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/eyedropper.ts
  8. 15 13
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/floodfill.ts
  9. 102 50
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/paintbrush.ts
  10. 63 3
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/rectangleSelect.ts
  11. 65 61
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/propertiesBar.tsx
  12. 233 41
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureCanvasManager.ts
  13. 53 47
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureEditor.scss
  14. 57 14
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureEditorComponent.tsx
  15. 7 5
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/toolBar.tsx
  16. 15 0
      inspector/src/lod.ts
  17. 33 0
      inspector/src/lodCube.ts
  18. 28 18
      inspector/src/textureHelper.ts

+ 1 - 1
dist/preview release/what's new.md

@@ -83,7 +83,7 @@
 - Popup Window available (To be used in Curve Editor) ([pixelspace](https://github.com/devpixelspace))
 - Add support to update inspector when switching to a new scene ([belfortk](https://github.com/belfortk))
 - Hex Component for Hex inputs on layer masks. ([msDestiny14](https://github.com/msDestiny14))
-- View & edit textures in pop out inspector using canvas and postprocesses. ([DarraghBurkeMS](https://github.com/DarraghBurkeMS))
+- View & edit textures in pop out inspector using canvas and postprocesses. Supports region selection and individual channel editing. ([DarraghBurkeMS](https://github.com/DarraghBurkeMS))
 
 ### Cameras
 

+ 4 - 1
inspector/src/components/actionTabs/actionTabs.scss

@@ -801,7 +801,10 @@ $line-padding-left: 2px;
                         width: 256px;
                         margin-top: 5px;
                         margin-bottom: 5px;
-                        border: 2px solid rgba(255, 255, 255, 0.4);
+                        border: 1px solid white;
+                        background-size: 32px 32px;
+                        background-color: white;
+                        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2 2'%3E%3Cpath fill='rgba(1.0,1.0,1.0,0.3)' fill-rule='evenodd' d='M0 0h1v1H0V0zm1 1h1v1H1V1z'/%3E%3C/svg%3E");
                     }
                 }
 

+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/texturePropertyGridComponent.tsx

@@ -191,6 +191,7 @@ export class TexturePropertyGridComponent extends React.Component<ITextureProper
                         texture={this.props.texture}
                         url={textureUrl}
                         window={this.popoutWindowRef}
+                        onUpdate={() => this.forceRefresh()}
                     />
                 </PopupComponent>)}
                 <CustomPropertyGridComponent globalState={this.props.globalState} target={texture}

+ 3 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/bottomBar.tsx

@@ -2,12 +2,15 @@ import * as React from 'react';
 
 interface BottomBarProps {
     name: string;
+    mipLevel: number;
+    hasMips: boolean;
 }
 
 export class BottomBar extends React.Component<BottomBarProps> {
     render() {
         return <div id='bottom-bar'>
             <span id='file-url'>{this.props.name}</span>
+            {this.props.hasMips && <span id='mip-level'>MIP Preview: {this.props.mipLevel}</span>}
         </div>;
     }
 }

+ 10 - 2
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/contrast.ts

@@ -35,11 +35,19 @@ export const Contrast : IToolData = {
         }
         /** Maps slider values to post processing values using an exponential regression */
         computeExposure(sliderValue : number) {
-            return Math.pow(1.05698, sliderValue) + 0.0000392163 * sliderValue;
+            if (sliderValue <= 0) {
+                return 1 - (-sliderValue / 100);
+            } else {
+                return Math.pow(1.05698, sliderValue) + 0.0000392163 * sliderValue;
+            }
         }
         /** Maps slider values to post processing values using an exponential regression */
         computeContrast(sliderValue : number) {
-            return Math.pow(1.05698, sliderValue) + 0.0000392163 * sliderValue;
+            if (sliderValue <= 0) {
+                return 1 - (-sliderValue / 100);
+            } else {
+                return Math.pow(1.05698, sliderValue) + 0.0000392163 * sliderValue;
+            }
         }
         setup() {
             this.contrast = 0;

+ 2 - 1
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/defaultTools.ts

@@ -2,5 +2,6 @@ import { Paintbrush } from './paintbrush';
 import { Eyedropper } from './eyedropper';
 import { Floodfill } from './floodfill';
 import { Contrast } from './contrast';
+import { RectangleSelect } from './rectangleSelect';
 
-export default [Paintbrush, Eyedropper, Floodfill, Contrast];
+export default [RectangleSelect, Paintbrush, Eyedropper, Floodfill, Contrast];

+ 13 - 13
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/eyedropper.ts

@@ -1,11 +1,14 @@
 import { IToolParameters, IToolData } from '../textureEditorComponent';
 import { PointerEventTypes, PointerInfo } from 'babylonjs/Events/pointerEvents';
+import { Nullable } from 'babylonjs/types'
+import { Observer } from 'babylonjs/Misc/observable';
+import { Color3 } from 'babylonjs/Maths/math.color';
 
 export const Eyedropper : IToolData = {
     name: 'Eyedropper',
     type: class {
         getParameters: () => IToolParameters;
-        pointerObservable: any;
+        pointerObserver: Nullable<Observer<PointerInfo>>;
         isPicking: boolean;
 
         constructor(getParameters: () => IToolParameters) {
@@ -13,18 +16,18 @@ export const Eyedropper : IToolData = {
         }
 
         pick(pointerInfo : PointerInfo) {
-            const p = this.getParameters();
-            const ctx = p.canvas2D.getContext('2d');
-            const {x, y} = p.getMouseCoordinates(pointerInfo);
+            const {canvas2D, setMetadata, getMouseCoordinates} = this.getParameters();
+            const ctx = canvas2D.getContext('2d');
+            const {x, y} = getMouseCoordinates(pointerInfo);
             const pixel = ctx!.getImageData(x, y, 1, 1).data;
-            p.setMetadata({
-                color: '#' + ('000000' + this.rgbToHex(pixel[0], pixel[1], pixel[2])).slice(-6),
-                opacity: pixel[3] / 255
+            setMetadata({
+                color: Color3.FromInts(pixel[0], pixel[1], pixel[2]).toHexString(),
+                alpha: pixel[3] / 255
             });
         }
         
         setup () {
-            this.pointerObservable = this.getParameters().scene.onPointerObservable.add((pointerInfo) => {
+            this.pointerObserver = this.getParameters().scene.onPointerObservable.add((pointerInfo) => {
                 if (pointerInfo.pickInfo?.hit) {
                     if (pointerInfo.type === PointerEventTypes.POINTERDOWN) {
                         this.isPicking = true;
@@ -41,13 +44,10 @@ export const Eyedropper : IToolData = {
             this.isPicking = false;
         }
         cleanup () {
-            if (this.pointerObservable) {
-                this.getParameters().scene.onPointerObservable.remove(this.pointerObservable);
+            if (this.pointerObserver) {
+                this.getParameters().scene.onPointerObservable.remove(this.pointerObserver);
             }
         }
-        rgbToHex(r: number, g:number, b: number) {
-            return ((r << 16) | (g << 8) | b).toString(16);
-        }
     },
     icon: `PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMCAwIDQwIDQwIj48cmVjdCB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHN0eWxlPSJmaWxsOm5vbmUiLz48cGF0aCBkPSJNMjkuMzIsMTAu
     NjhjLTEuNjYtMS42Ni00LjA2LTEtNS41Ni41YTExLjg5LDExLjg5LDAsMCwwLTEuNjYsMi4zMUwyMiwxMy40MWExLjg5LDEuODksMCwwLDAtMi42NiwwbC0uOS45YTEuODksMS44OSwwLDAsMC0uMjIsMi4zOWwtNi4wOSw2LjA5YTIuNzUsMi43NSwwLDAsMC0uNzMs

+ 15 - 13
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/floodfill.ts

@@ -1,28 +1,30 @@
 import { IToolParameters, IToolData } from '../textureEditorComponent';
-import { PointerEventTypes } from 'babylonjs/Events/pointerEvents';
+import { PointerEventTypes, PointerInfo } from 'babylonjs/Events/pointerEvents';
+import { Nullable } from 'babylonjs/types'
+import { Observer } from 'babylonjs/Misc/observable';
 
 export const Floodfill : IToolData = {
     name: 'Floodfill',
     type: class {
         getParameters: () => IToolParameters;
-        pointerObservable: any;
-
+        pointerObserver: Nullable<Observer<PointerInfo>>;
         constructor(getParameters: () => IToolParameters) {
             this.getParameters = getParameters;
         }
 
         fill() {
-            const p = this.getParameters();
-            const ctx = p.canvas2D.getContext('2d')!;
-            ctx.fillStyle = p.metadata.color;
-            ctx.globalAlpha = p.metadata.opacity;
-            ctx.globalCompositeOperation = 'source-over';
-            ctx.fillRect(0,0, p.size.width, p.size.height);
-            p.updateTexture();
+            const {metadata, startPainting, updatePainting, stopPainting} = this.getParameters();
+            const ctx = startPainting();
+            ctx.fillStyle = metadata.color;
+            ctx.globalAlpha = metadata.alpha;
+            ctx.globalCompositeOperation = 'copy';
+            ctx.fillRect(0,0, ctx.canvas.width, ctx.canvas.height);
+            updatePainting();
+            stopPainting();
         }
         
         setup () {
-            this.pointerObservable = this.getParameters().scene.onPointerObservable.add((pointerInfo) => {
+            this.pointerObserver = this.getParameters().scene.onPointerObservable.add((pointerInfo) => {
                 if (pointerInfo.pickInfo?.hit) {
                     if (pointerInfo.type === PointerEventTypes.POINTERDOWN) {
                         this.fill();
@@ -31,8 +33,8 @@ export const Floodfill : IToolData = {
             });
         }
         cleanup () {
-            if (this.pointerObservable) {
-                this.getParameters().scene.onPointerObservable.remove(this.pointerObservable);
+            if (this.pointerObserver) {
+                this.getParameters().scene.onPointerObservable.remove(this.pointerObserver);
             }
         }
     },

文件差異過大導致無法顯示
+ 102 - 50
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/paintbrush.ts


文件差異過大導致無法顯示
+ 63 - 3
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/rectangleSelect.ts


+ 65 - 61
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/propertiesBar.tsx

@@ -11,6 +11,8 @@ interface IPropertiesBarProps {
     resetTexture() : void;
     resizeTexture(width: number, height: number) : void;
     uploadTexture(file : File) : void;
+    mipLevel: number;
+    setMipLevel: (mipLevel : number) => void;
 }
 
 interface IPropertiesBarState {
@@ -20,7 +22,7 @@ interface IPropertiesBarState {
 
 interface IPixelDataProps {
     name : string;
-    data?: number;
+    data: number | undefined;
 }
 
 export class PropertiesBar extends React.Component<IPropertiesBarProps,IPropertiesBarState> {
@@ -54,7 +56,7 @@ export class PropertiesBar extends React.Component<IPropertiesBarProps,IProperti
     }
 
     private pixelData(props: IPixelDataProps) {
-        return <span className='pixel-data'>{props.name}: <span className='value'>{props.data || '-'}</span></span>;
+        return <span className='pixel-data'>{props.name}: <span className='value'>{props.data !== undefined ? props.data : '-'}</span></span>;
     }
 
     private getNewDimension(oldDim : number, newDim : any) {
@@ -68,76 +70,78 @@ export class PropertiesBar extends React.Component<IPropertiesBarProps,IProperti
     }
 
     render() {
+        const {mipLevel, setMipLevel, pixelData, resizeTexture, texture, face, setFace, saveTexture, resetTexture, uploadTexture} = this.props;
         return <div id='properties'>
                 <div className='tab' id='logo-tab'>
                     <img className='icon' src={this._babylonLogo}/>
                 </div>
-                <div className='tab' id='dimensions-tab'>
-                    <form onSubmit={evt => {
-                        this.props.resizeTexture(this.state.width, this.state.height);
-                        evt.preventDefault();
-                    }}>
-                        <label className='dimensions'>
-                            W: <input type='text' value={this.state.width} onChange={(evt) => this.setState({width: this.getNewDimension(this.state.width, evt.target.value)})}/>
+                <div id='left'>
+                    <div className='tab' id='dimensions-tab'>
+                        <form onSubmit={evt => {
+                            this.props.resizeTexture(this.state.width, this.state.height);
+                            evt.preventDefault();
+                        }}>
+                            <label className='dimensions'>
+                                W: <input type='text' value={this.state.width} readOnly={texture.isCube} onChange={(evt) => this.setState({width: this.getNewDimension(this.state.width, evt.target.value)})}/>
                             </label>
-                        <label className='dimensions'>
-                            H: <input type='text' value={this.state.height} onChange={(evt) => this.setState({height: this.getNewDimension(this.state.height, evt.target.value)})}/>
+                            <label className='dimensions'>
+                                H: <input type='text' value={this.state.height} readOnly={texture.isCube} onChange={(evt) => this.setState({height: this.getNewDimension(this.state.height, evt.target.value)})}/>
                             </label>
-                        <img id='resize' className='icon button' title='Resize' alt='Resize' src={this._resizeButton} onClick={() => this.props.resizeTexture(this.state.width, this.state.height)}/> 
-                    </form>
-                </div>
-                <div className='tab' id='pixel-coords-tab'>
-                    <this.pixelData name='X' data={this.props.pixelData.x}/>
-                    <this.pixelData name='Y' data={this.props.pixelData.y}/>
-                </div>
-                <div className='tab' id='pixel-color-tab'>
-                    <this.pixelData name='R' data={this.props.pixelData.r}/>
-                    <this.pixelData name='G' data={this.props.pixelData.g}/>
-                    <this.pixelData name='B' data={this.props.pixelData.b}/>
-                    <this.pixelData name='A' data={this.props.pixelData.a}/>
-                </div>
-                {this.props.texture.isCube &&
-                <>
-                    <div className='tab' id='face-tab'>
-                        {this._faces.map((face, index) =>
-                        <img
-                            key={index}
-                            className={this.props.face == index ? 'icon face button active' : 'icon face button'}
-                            src={face}
-                            onClick={() => this.props.setFace(index)}
-                        />)}
+                        {!texture.isCube && <img id='resize' className='icon button' title='Resize' alt='Resize' src={this._resizeButton} onClick={() => resizeTexture(this.state.width, this.state.height)}/>} 
+                        </form>
+                    </div>
+                    <div className='tab' id='pixel-coords-tab'>
+                        <this.pixelData name='X' data={pixelData.x}/>
+                        <this.pixelData name='Y' data={pixelData.y}/>
                     </div>
-                    <div className='tab' id='mip-tab'>
-                        <img title='Mip Preview Up' className='icon button' src={this._mipUp} />
-                        <img title='Mip Preview Down' className='icon button' src={this._mipDown} />
+                    <div className='tab' id='pixel-color-tab'>
+                        <this.pixelData name='R' data={pixelData.r}/>
+                        <this.pixelData name='G' data={pixelData.g}/>
+                        <this.pixelData name='B' data={pixelData.b}/>
+                        <this.pixelData name='A' data={pixelData.a}/>
                     </div>
-                </>}
+                    {texture.isCube &&
+                        <div className='tab' id='face-tab'>
+                            {this._faces.map((value, index) =>
+                            <img
+                                key={index}
+                                className={face == index ? 'icon face button active' : 'icon face button'}
+                                src={value}
+                                onClick={() => setFace(index)}
+                            />)}
+                        </div>
+                    }
+                    {!texture.noMipmap &&
+                        <div className='tab' id='mip-tab'>
+                            <img title='Mip Preview Up' className='icon button' src={this._mipUp} onClick={() => mipLevel > 0 && setMipLevel(mipLevel - 1)} />
+                            <img title='Mip Preview Down' className='icon button' src={this._mipDown} onClick={() => mipLevel < 12 && setMipLevel(mipLevel + 1)} />
+                        </div>
+                    }
+                </div>
                 <div className='tab' id='right-tab'>
-                    <div className='content'>
-                        <img title='Reset' className='icon button' src={this._resetButton} onClick={() => this.props.resetTexture()}/>
-                        <label>
-                            <input
-                                accept='.jpg, .png, .tga, .dds, .env'
-                                type='file'
-                                onChange={
-                                    (evt : React.ChangeEvent<HTMLInputElement>) => {
-                                        const files = evt.target.files;
-                                        if (files && files.length) {
-                                            this.props.uploadTexture(files[0]);
-                                        }
-                                
-                                        evt.target.value = "";
+                    <img title='Reset' className='icon button' src={this._resetButton} onClick={() => resetTexture()}/>
+                    <label>
+                        <input
+                            accept='.jpg, .png, .tga, .dds, .env'
+                            type='file'
+                            onChange={
+                                (evt : React.ChangeEvent<HTMLInputElement>) => {
+                                    const files = evt.target.files;
+                                    if (files && files.length) {
+                                        uploadTexture(files[0]);
                                     }
+                            
+                                    evt.target.value = "";
                                 }
-                            />
-                            <img
-                                title='Upload'
-                                className='icon button'
-                                src={this._uploadButton}
-                            />
-                        </label>
-                        <img title='Save' className='icon button' src={this._saveButton} onClick={() => this.props.saveTexture()}/>
-                    </div>
+                            }
+                        />
+                        <img
+                            title='Upload'
+                            className='icon button'
+                            src={this._uploadButton}
+                        />
+                    </label>
+                    <img title='Save' className='icon button' src={this._saveButton} onClick={() => saveTexture()}/>
                 </div>
         </div>;
     }

+ 233 - 41
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureCanvasManager.ts

@@ -34,6 +34,7 @@ import { StackPanel } from 'babylonjs-gui/2D/controls/stackPanel';
 import { Control } from 'babylonjs-gui/2D/controls/control';
 import { Style } from 'babylonjs-gui/2D/style';
 import { AdvancedDynamicTexture } from 'babylonjs-gui/2D/advancedDynamicTexture';
+import { IMetadata } from './textureEditorComponent';
 
 
 export interface IPixelData {
@@ -57,6 +58,7 @@ export class TextureCanvasManager {
     private _engine: Engine;
     private _scene: Scene;
     private _camera: FreeCamera;
+    private _cameraPos: Vector2;
 
     private _scale : number;
     private _isPanning : boolean = false;
@@ -81,6 +83,7 @@ export class TextureCanvasManager {
 
     private _channels : IChannel[] = [];
     private _face : number = 0;
+    private _mipLevel : number = 0;
 
     /* The texture from the original engine that we invoked the editor on */
     private _originalTexture: BaseTexture;
@@ -108,6 +111,9 @@ export class TextureCanvasManager {
     private static MIN_SCALE : number = 0.01;
     private static MAX_SCALE : number = 10;
 
+    private static SELECT_ALL_KEY = 'KeyA';
+    private static DESELECT_KEY = 'Escape'
+
     private _tool : Nullable<ITool>;
 
     private _setPixelData : (pixelData : IPixelData) => void;
@@ -116,24 +122,39 @@ export class TextureCanvasManager {
 
     private _window : Window;
 
-    public metadata : any = {};
+    private _metadata : IMetadata;
 
     private _editing3D : boolean = false;
 
+    private _onUpdate : () => void;
+    private _setMetadata : (metadata: any) => void;
+
+    private _imageData : Uint8Array | Uint8ClampedArray;
+    private _canUpdate : boolean = true;
+    private _shouldUpdate : boolean = false;
+    private _paintCanvas: HTMLCanvasElement;
+
     public constructor(
         texture: BaseTexture,
         window: Window,
         canvasUI: HTMLCanvasElement,
         canvas2D: HTMLCanvasElement,
         canvas3D: HTMLCanvasElement,
-        setPixelData: (pixelData : IPixelData) => void
+        setPixelData: (pixelData : IPixelData) => void,
+        metadata: IMetadata,
+        onUpdate: () => void,
+        setMetadata: (metadata: any) => void
     ) {
         this._window = window;
 
         this._UICanvas = canvasUI;
         this._2DCanvas = canvas2D;
         this._3DCanvas = canvas3D;
+        this._paintCanvas = document.createElement('canvas');
         this._setPixelData = setPixelData;
+        this._metadata = metadata;
+        this._onUpdate = onUpdate;
+        this._setMetadata = setMetadata;
 
         this._size = texture.getSize();
         this._originalTexture = texture;
@@ -144,8 +165,9 @@ export class TextureCanvasManager {
 
         this._camera = new FreeCamera('camera', new Vector3(0, 0, -1), this._scene);
         this._camera.mode = Camera.ORTHOGRAPHIC_CAMERA;
+        this._cameraPos = new Vector2();
 
-        this._channelsTexture = new HtmlElementTexture('ct', this._2DCanvas, {engine: this._engine, scene: null, samplingMode: Engine.TEXTURE_NEAREST_LINEAR});
+        this._channelsTexture = new HtmlElementTexture('ct', this._2DCanvas, {engine: this._engine, scene: null, samplingMode: Texture.NEAREST_SAMPLINGMODE, generateMipMaps: true});
 
         this._3DEngine = new Engine(this._3DCanvas);
         this._3DScene = new Scene(this._3DEngine);
@@ -156,15 +178,13 @@ export class TextureCanvasManager {
         cam.mode = Camera.ORTHOGRAPHIC_CAMERA;
         [cam.orthoBottom, cam.orthoLeft, cam.orthoTop, cam.orthoRight] = [-0.5, -0.5, 0.5, 0.5];
         this._3DPlane = PlaneBuilder.CreatePlane('texture', {width: 1, height: 1}, this._3DScene);
+        this._3DPlane.hasVertexAlpha = true;
         const mat = new StandardMaterial('material', this._3DScene);
         mat.diffuseTexture = this._3DCanvasTexture;
+        mat.useAlphaFromDiffuseTexture = true;
         mat.disableLighting = true;
         mat.emissiveColor = Color3.White();
         this._3DPlane.material = mat;
-        
-
-        this.grabOriginalTexture();
-
 
         this._planeMaterial = new ShaderMaterial(
             'shader',
@@ -194,11 +214,23 @@ export class TextureCanvasManager {
                     uniform bool g;
                     uniform bool b;
                     uniform bool a;
+
+                    uniform int x1;
+                    uniform int y1;
+                    uniform int x2;
+                    uniform int y2;
+                    uniform int w;
+                    uniform int h;
+
+                    uniform int time;
             
                     varying vec2 vUV;
+
+                    float scl = 200.0;
+                    float speed = 10.0 / 1000.0;
+                    float smoothing = 0.2;
             
                     void main(void) {
-                        float size = 20.0;
                         vec2 pos2 = vec2(gl_FragCoord.x, gl_FragCoord.y);
                         vec2 pos = floor(pos2 * 0.05);
                         float pattern = mod(pos.x + pos.y, 2.0); 
@@ -244,20 +276,42 @@ export class TextureCanvasManager {
                                 col.a = 1.0;
                             }
                         }
-                        gl_FragColor = col;
                         gl_FragColor = col * (col.a) + bg * (1.0 - col.a);
+                        float wF = float(w);
+                        float hF = float(h);
+                        int xPixel = int(floor(vUV.x * wF));
+                        int yPixel = int(floor((1.0 - vUV.y) * hF));
+                        int xDis = min(abs(xPixel - x1), abs(xPixel - x2));
+                        int yDis = min(abs(yPixel - y1), abs(yPixel - y2));
+                        if (xPixel >= x1 && yPixel >= y1 && xPixel <= x2 && yPixel <= y2) {
+                            if (xDis <= 4 || yDis <= 4) {
+                                float c = sin(vUV.x * scl + vUV.y * scl + float(time) * speed);
+                                c = smoothstep(-smoothing,smoothing,c);
+                                float val = 1.0 - c;
+                                gl_FragColor = vec4(val, val, val, 1.0) * 0.7 + gl_FragColor * 0.3;
+                            }
+                        }
                     }`
             },
         {
             attributes: ['position', 'uv'],
-            uniforms: ['worldViewProjection', 'textureSampler', 'r', 'g', 'b', 'a']
+            uniforms: ['worldViewProjection', 'textureSampler', 'r', 'g', 'b', 'a', 'x1', 'y1', 'x2', 'y2', 'w', 'h', 'time']
         });
+        
+        this.grabOriginalTexture();
 
         this._planeMaterial.setTexture('textureSampler', this._channelsTexture);
         this._planeMaterial.setFloat('r', 1.0);
         this._planeMaterial.setFloat('g', 1.0);
         this._planeMaterial.setFloat('b', 1.0);
         this._planeMaterial.setFloat('a', 1.0);
+        this._planeMaterial.setInt('x1', -1);
+        this._planeMaterial.setInt('y1', -1);
+        this._planeMaterial.setInt('x2', -1);
+        this._planeMaterial.setInt('y2', -1);
+        this._planeMaterial.setInt('w', this._size.width);
+        this._planeMaterial.setInt('h', this._size.height);
+        this._planeMaterial.setInt('time', 0);
         this._plane.material = this._planeMaterial;
         
         const adt = AdvancedDynamicTexture.CreateFullscreenUI('gui', true, this._scene);
@@ -284,7 +338,7 @@ export class TextureCanvasManager {
         topBar.background = '#666666';
         topBar.thickness = 0;
         topBar.hoverCursor = 'grab';
-        topBar.onPointerDownObservable.add(evt => {this._GUI.isDragging = true; topBar.hoverCursor = 'grabbing';});
+        topBar.onPointerDownObservable.add(() => {this._GUI.isDragging = true; topBar.hoverCursor = 'grabbing';});
         topBar.onPointerUpObservable.add(() => {this._GUI.isDragging = false; this._GUI.dragCoords = null; topBar.hoverCursor = 'grab';});
 
         const title = new TextBlock();
@@ -295,25 +349,53 @@ export class TextureCanvasManager {
         topBar.addControl(title);
         this._GUI.toolWindow.addControl(topBar);
 
-        this._window.addEventListener('pointermove',  (evt : PointerEvent) => {
+        this._window.addEventListener('pointermove', evt => {
             if (!this._GUI.isDragging) return;
             if (!this._GUI.dragCoords) {
                 this._GUI.dragCoords = new Vector2(evt.x, evt.y);
                 return;
             }
-            let x = parseInt(this._GUI.toolWindow.left.toString().replace('px', ''));
-            let y = parseInt(this._GUI.toolWindow.top.toString().replace('px', ''));
-            x += evt.x - this._GUI.dragCoords.x;
-            y += evt.y - this._GUI.dragCoords.y;
-            this._GUI.toolWindow.left = `${x}px`;
-            this._GUI.toolWindow.top = `${y}px`;
+            this._GUI.toolWindow.leftInPixels += evt.x - this._GUI.dragCoords.x;
+            this._GUI.toolWindow.topInPixels += evt.y - this._GUI.dragCoords.y;
             this._GUI.dragCoords.x = evt.x;
             this._GUI.dragCoords.y = evt.y;
         });
 
+        this._window.addEventListener('keydown', evt => {
+            this._keyMap[evt.code] = true;
+            if (evt.code === TextureCanvasManager.SELECT_ALL_KEY && evt.ctrlKey) {
+                this._setMetadata({
+                    select: {
+                        x1: 0,
+                        y1: 0,
+                        x2: this._size.width,
+                        y2: this._size.height
+                    }
+                });
+                evt.preventDefault();
+            }
+            if (evt.code === TextureCanvasManager.DESELECT_KEY) {
+                this._setMetadata({
+                    select: {
+                        x1: -1,
+                        y1: -1,
+                        x2: -1,
+                        y2: -1
+                    }
+                })
+            }
+        });
+        
+        this._window.addEventListener('keyup', evt => {
+            this._keyMap[evt.code] = false;
+        });
+
         this._engine.runRenderLoop(() => {
             this._engine.resize();
+            this.GUI.toolWindow.left = Math.min(Math.max(this._GUI.toolWindow.leftInPixels, -this._UICanvas.width + this._GUI.toolWindow.widthInPixels), 0);
+            this.GUI.toolWindow.top = Math.min(Math.max(this._GUI.toolWindow.topInPixels, -this._UICanvas.height + this._GUI.toolWindow.heightInPixels), 0);
             this._scene.render();
+            this._planeMaterial.setInt('time', new Date().getTime());
         });
 
         this._scale = 1.5;
@@ -322,10 +404,11 @@ export class TextureCanvasManager {
         this._scene.onBeforeRenderObservable.add(() => {
             this._scale = Math.min(Math.max(this._scale, TextureCanvasManager.MIN_SCALE), TextureCanvasManager.MAX_SCALE);
             const ratio = this._UICanvas?.width / this._UICanvas?.height;
-            this._camera.orthoBottom = -1 / this._scale;
-            this._camera.orthoTop = 1 / this._scale;
-            this._camera.orthoLeft =  ratio / -this._scale;
-            this._camera.orthoRight = ratio / this._scale;
+            const {x,y} = this._cameraPos;
+            this._camera.orthoBottom = y - 1 / this._scale;
+            this._camera.orthoTop = y + 1 / this._scale;
+            this._camera.orthoLeft =  x - ratio / this._scale;
+            this._camera.orthoRight = x + ratio / this._scale;
         })
 
         this._scene.onPointerObservable.add((pointerInfo) => {
@@ -349,16 +432,15 @@ export class TextureCanvasManager {
                     break;
                 case PointerEventTypes.POINTERMOVE:
                     if (this._isPanning) {
-                        this._camera.position.x -= (pointerInfo.event.x - this._mouseX) / this._scale * TextureCanvasManager.PAN_SPEED;
-                        this._camera.position.y += (pointerInfo.event.y - this._mouseY) / this._scale * TextureCanvasManager.PAN_SPEED;
+                        this._cameraPos.x -= (pointerInfo.event.x - this._mouseX) / this._scale * TextureCanvasManager.PAN_SPEED;
+                        this._cameraPos.y += (pointerInfo.event.y - this._mouseY) / this._scale * TextureCanvasManager.PAN_SPEED;
                         this._mouseX = pointerInfo.event.x;
                         this._mouseY = pointerInfo.event.y;
                     }
                     if (pointerInfo.pickInfo?.hit) {
                         const pos = this.getMouseCoordinates(pointerInfo);
-                        const ctx = this._2DCanvas.getContext('2d');
-                        const pixel = ctx?.getImageData(pos.x, pos.y, 1, 1).data!;
-                        this._setPixelData({x: pos.x, y: pos.y, r:pixel[0], g:pixel[1], b:pixel[2], a:pixel[3]});
+                        const idx = (pos.x + pos.y * this._size.width) * 4;
+                        this._setPixelData({x: pos.x, y: pos.y, r:this._imageData[idx], g:this._imageData[idx + 1], b:this._imageData[idx + 2], a:this._imageData[idx + 3]});
                     } else {
                         this._setPixelData({});
                     }
@@ -370,11 +452,13 @@ export class TextureCanvasManager {
             switch(kbInfo.type) {
                 case KeyboardEventTypes.KEYDOWN:
                     this._keyMap[kbInfo.event.key] = true;
-                    if (kbInfo.event.key === TextureCanvasManager.ZOOM_IN_KEY) {
-                        this._scale += TextureCanvasManager.ZOOM_KEYBOARD_SPEED * this._scale;
-                    }
-                    if (kbInfo.event.key === TextureCanvasManager.ZOOM_OUT_KEY) {
-                        this._scale -= TextureCanvasManager.ZOOM_KEYBOARD_SPEED * this._scale;
+                    switch (kbInfo.event.key) {
+                        case TextureCanvasManager.ZOOM_IN_KEY:
+                            this._scale += TextureCanvasManager.ZOOM_KEYBOARD_SPEED * this._scale;
+                            break;
+                        case TextureCanvasManager.ZOOM_OUT_KEY:
+                            this._scale -= TextureCanvasManager.ZOOM_KEYBOARD_SPEED * this._scale;
+                            break;
                     }
                     break;
                 case KeyboardEventTypes.KEYUP:
@@ -384,6 +468,7 @@ export class TextureCanvasManager {
         });
     }
 
+
     public async updateTexture() {
         this._didEdit = true;
         const element = this._editing3D ? this._3DCanvas : this._2DCanvas;
@@ -407,16 +492,102 @@ export class TextureCanvasManager {
             } else {
                 (this._target as HtmlElementTexture).element = element;
             }
-            (this._target as HtmlElementTexture).update((this._originalTexture as Texture).invertY);
+            this.queueTextureUpdate();
         }
         this._originalTexture._texture = this._target._texture;
         this._channelsTexture.element = element;
         this.updateDisplay();
+        this._onUpdate();
+    }
+
+    private queueTextureUpdate() {
+        if (this._canUpdate) {
+            (this._target as HtmlElementTexture).update((this._originalTexture as Texture).invertY);
+            if (this._editing3D) {
+                this._imageData = this._3DEngine.readPixels(0, 0, this._size.width, this._size.height);
+            } else {
+                this._imageData = this._2DCanvas.getContext('2d')!.getImageData(0, 0, this._size.width, this._size.height).data;
+            }
+            this._canUpdate = false;
+            this._shouldUpdate = false;
+            setTimeout(() => {
+                this._canUpdate = true;
+                if (this._shouldUpdate) {
+                    this.queueTextureUpdate();
+                }
+            }, 32);
+        } else {
+            this._shouldUpdate = true;
+        }
+    }
+
+    public startPainting() : CanvasRenderingContext2D {
+        let x = 0, y = 0, w = this._size.width, h = this._size.height;
+        if (this._metadata.select.x1 != -1) {
+            x = this._metadata.select.x1;
+            y = this._metadata.select.y1;
+            w = this._metadata.select.x2 - this._metadata.select.x1;
+            h = this._metadata.select.y2 - this._metadata.select.y1;
+        }
+        this._paintCanvas.width = w;
+        this._paintCanvas.height = h;
+        const ctx = this._paintCanvas.getContext('2d')!;
+        ctx.imageSmoothingEnabled = false;
+        ctx.drawImage(this._2DCanvas, x, y, w, h, 0, 0, w, h);
+        return ctx;
+    }
+
+    public updatePainting() {
+        let x = 0, y = 0, w = this._size.width, h = this._size.height;
+        if (this._metadata.select.x1 != -1) {
+            x = this._metadata.select.x1;
+            y = this._metadata.select.y1;
+            w = this._metadata.select.x2 - this._metadata.select.x1;
+            h = this._metadata.select.y2 - this._metadata.select.y1;
+        }
+        let editingAllChannels = true;
+        this._channels.forEach(channel => {
+            if (!channel.editable) editingAllChannels = false;
+        })
+        let oldData : Uint8ClampedArray;
+        if (!editingAllChannels) {
+            oldData = this._2DCanvas.getContext('2d')!.getImageData(x, y, w, h).data;
+        }
+        const ctx = this._paintCanvas.getContext('2d')!;
+        const ctx2D = this.canvas2D.getContext('2d')!;
+        ctx2D.globalAlpha = 1.0;
+        ctx2D.globalCompositeOperation = 'destination-out';
+        ctx2D.fillStyle = 'white';
+        ctx2D.fillRect(x,y,w,h);
+        ctx2D.imageSmoothingEnabled = false;
+        if (!editingAllChannels) {
+            const newData = ctx.getImageData(0, 0, w, h);
+            const nd = newData.data;
+            this._channels.forEach((channel, index) => {
+                if (!channel.editable) {
+                    for(let i = index; i < w * h * 4; i += 4) {
+                        nd[i] = oldData[i];
+                    }
+                }
+            });
+            ctx2D.globalCompositeOperation = 'source-over';
+            ctx2D.globalAlpha = 1.0;
+            ctx2D.putImageData(newData, x, y);
+        } else {
+            ctx2D.globalCompositeOperation = 'source-over';
+            ctx2D.globalAlpha = 1.0;
+            ctx2D.drawImage(ctx.canvas, x, y);
+        }
+        this.updateTexture();
+    }
+
+    public stopPainting() : void {
+        this._paintCanvas.getContext('2d')!.clearRect(0, 0, this._paintCanvas.width, this._paintCanvas.height);
     }
 
     private updateDisplay() {
-        this._3DScene.render()
-        this._channelsTexture.update();
+        this._3DScene.render();
+        this._channelsTexture.update(true);
     }
 
     public set channels(channels: IChannel[]) {
@@ -456,8 +627,11 @@ export class TextureCanvasManager {
             this._size.width,
             this._size.height,
             this._face,
-            {R:true ,G:true ,B:true ,A:true}
+            {R:true, G:true, B:true, A:true},
+            undefined,
+            this._mipLevel
         ).then(data => {
+            this._imageData = data;
             TextureCanvasManager.paintPixelsOnCanvas(data, this._2DCanvas);
             this._3DCanvasTexture.update();
             this.updateDisplay();
@@ -513,7 +687,6 @@ export class TextureCanvasManager {
         return this._tool;
     }
 
-    // BROKEN : FIX THIS
     public set face(face: number) {
         if (this._face !== face) {
             this._face = face;
@@ -522,6 +695,12 @@ export class TextureCanvasManager {
         }
     }
 
+    public set mipLevel(mipLevel : number) {
+        if (this._mipLevel === mipLevel) return;
+        this._mipLevel = mipLevel;
+        this.grabOriginalTexture(false);
+    }
+
     /** Returns the tool GUI object, allowing tools to access the GUI */
     public get GUI() {
         return this._GUI;
@@ -532,6 +711,15 @@ export class TextureCanvasManager {
         return this._3DScene;
     }
 
+    public set metadata(metadata: IMetadata) {
+        this._metadata = metadata;
+        const {x1,y1,x2,y2} = metadata.select;
+        this._planeMaterial.setInt('x1', x1);
+        this._planeMaterial.setInt('y1', y1);
+        this._planeMaterial.setInt('x2', x2);
+        this._planeMaterial.setInt('y2', y2);
+    }
+
     private makePlane() {
         const textureRatio = this._size.width / this._size.height;
         if (this._plane) this._plane.dispose();
@@ -551,6 +739,7 @@ export class TextureCanvasManager {
         this.grabOriginalTexture();
         this.makePlane();
         this._didEdit = false;
+        this._onUpdate();
     }
 
     public async resize(newSize : ISize) {
@@ -567,9 +756,11 @@ export class TextureCanvasManager {
         this._2DCanvas.height = this._size.height;
         this._3DCanvas.width = this._size.width;
         this._3DCanvas.height = this._size.height;
+        this._planeMaterial.setInt('w', this._size.width);
+        this._planeMaterial.setInt('h', this._size.height);
         if (adjustZoom) {
-            this._camera.position.x = 0;
-            this._camera.position.y = 0;
+            this._cameraPos.x = 0;
+            this._cameraPos.y = 0;
             this._scale = 1.5 / (this._size.width/this._size.height);
         }
         this.makePlane();
@@ -607,7 +798,7 @@ export class TextureCanvasManager {
                         this._scene,
                         this._originalTexture.noMipmap,
                         false,
-                        Engine.TEXTURE_NEAREST_SAMPLINGMODE,
+                        Texture.NEAREST_SAMPLINGMODE,
                         () => {
                             TextureHelper.GetTextureDataAsync(texture, texture.getSize().width, texture.getSize().height, 0, {R: true, G: true, B: true, A: true})
                                 .then((pixels) => {
@@ -633,7 +824,8 @@ export class TextureCanvasManager {
         }
         if (this._tool) {
             this._tool.instance.cleanup();
-        }        
+        }
+        this._paintCanvas.parentNode?.removeChild(this._paintCanvas);
         this._3DPlane.dispose();
         this._3DCanvasTexture.dispose();
         this._3DScene.dispose();

+ 53 - 47
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureEditor.scss

@@ -47,70 +47,59 @@
     }
     
     #properties {
-        position: absolute;
         width: 100%;
         height: 40px;
         
         display: flex;
-        flex-flow: row nowrap;
         align-items: center;
         font-size: 12px;
         color: white;
         user-select: none;
-        background-color: #1e1e1e;
+        background-color: #333333;
     
         .tab {
-            display: flex;
+            display: inline-flex;
             line-height: 40px;
             height: 40px;
             flex-shrink: 0;
             flex-grow: 0;
-            margin-right: 2px;
+            border-right: 2px solid #1e1e1e;
             background-color: #333333;
         }
-        #dimensions-tab {
-            form {
-                display: flex;
-            }
-            label {
-                margin-left: 15px;
-                color: #afafaf;
-                input {
-                    width: 40px;
-                    height: 24px;
-                    background-color: #000000;
-                    color: #ffffff;
-                    border: 0;
-                    padding-left: 4px;
-                    font-size: 12px;
+        #left {
+            overflow: hidden;
+            height: 40px;
+            flex-grow: 1;
+            flex-shrink: 1;
+            display: flex;
+            flex-wrap: wrap;
+            #dimensions-tab {
+                form {
+                    display: flex;
                 }
-    
-                &:last-of-type {
-                    margin-right: 8px;
+                label {
+                    margin-left: 15px;
+                    color: #afafaf;
+                    input {
+                        width: 40px;
+                        height: 24px;
+                        background-color: #000000;
+                        color: #ffffff;
+                        border: 0;
+                        padding-left: 4px;
+                        font-size: 12px;
+                    }
+        
+                    &:last-of-type {
+                        margin-right: 8px;
+                    }
                 }
             }
-            @media only screen and (max-width: 334px) {
-                display: none;
-            }
-        }
-        #pixel-coords-tab {
-            @media only screen and (max-width: 440px) {
-                display: none;
-            }
-        }
-        #pixel-color-tab {
-            @media only screen and (max-width: 640px) {
-                display: none;
-            }
         }
         #right-tab {
-            flex-grow: 1;
-            flex-shrink: 1;
             margin-right: 0;
-            .content {
-                position: absolute;
-                right: 0px;
-            }
+            flex-grow: 0;
+            flex-shrink: 0;
             
             input[type="file"] {
                 display: none;
@@ -179,10 +168,22 @@
 
         #color {
             margin-top: 8px;
-            #activeColor {
+            #active-color-bg {
+                border-radius: 50%;
+                width: 20px;
+                height: 20px;
                 margin: 10px;
+                position: relative;
+                background-image: linear-gradient(45deg, #808080 25%, transparent 25%), linear-gradient(-45deg, #808080 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #808080 75%), linear-gradient(-45deg, transparent 75%, #808080 75%);
+                background-size: 20px 20px;
+                background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
+            }
+            #active-color {
                 width: 20px;
                 height: 20px;
+                position: absolute;
+                top: 0;
+                left: 0;
                 border-radius: 50%;
             }
         }
@@ -227,20 +228,25 @@
     
     #canvas-ui {
         width: 100%;
-        height: 100%;
+        height: calc(100% - 70px);
+        outline: none;
     }
     
     #bottom-bar {
-        position: absolute;
-        bottom: 0;
         height: 30px;
         width: 100%;
         background-color: #333333;
         font-size: 14px;
         user-select: none;
         line-height: 30px;
+        position: relative;
         #file-url {
-            margin-left: 30px;
+            left: 30px;
+            position: absolute;
+        }
+        #mip-level {
+            right: 30px;
+            position: absolute;
         }
     }
 }

+ 57 - 14
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureEditorComponent.tsx

@@ -24,21 +24,23 @@ interface ITextureEditorComponentProps {
     texture: BaseTexture;
     url: string;
     window: React.RefObject<PopupComponent>;
+    onUpdate: () => void;
 }
 
 interface ITextureEditorComponentState {
     tools: ITool[];
     activeToolIndex: number;
-    metadata: any;
+    metadata: IMetadata;
     channels: IChannel[];
     pixelData : IPixelData;
     face: number;
+    mipLevel: number;
 }
 
 export interface IToolParameters {
     /** The visible scene in the editor. Useful for adding pointer and keyboard events. */
     scene: Scene;
-    /** The 2D canvas which tools can paint on using the canvas API. */
+    /** The 2D canvas which you can sample pixel data from. Tools should not paint directly on this canvas. */
     canvas2D: HTMLCanvasElement;
     /** The 3D scene which tools can add post processes to. */
     scene3D: Scene;
@@ -47,7 +49,7 @@ export interface IToolParameters {
     /** Pushes the editor texture back to the original scene. This should be called every time a tool makes any modification to a texture. */
     updateTexture: () => void;
     /** The metadata object which is shared between all tools. Feel free to store any information here. Do not set this directly: instead call setMetadata. */
-    metadata: any;
+    metadata: IMetadata;
     /** Call this when you want to mutate the metadata. */
     setMetadata: (data : any) => void;
     /** Returns the texture coordinates under the cursor */
@@ -56,6 +58,12 @@ export interface IToolParameters {
     GUI: IToolGUI;
     /** Provides access to the BABYLON namespace */
     BABYLON: any;
+    /** Provides a canvas that you can use the canvas API to paint on. */
+    startPainting: () => CanvasRenderingContext2D;
+    /** After you have painted on your canvas, call this method to push the updates back to the texture. */
+    updatePainting: () => void;
+    /** Call this when you are finished painting. */
+    stopPainting: () => void;
 }
 
 
@@ -87,6 +95,18 @@ interface IToolConstructable {
     new (getParameters: () => IToolParameters) : IToolType;
 }
 
+export interface IMetadata {
+    color: string;
+    alpha: number;
+    select: {
+        x1: number,
+        y1: number,
+        x2: number,
+        y2: number
+    }
+    [key: string] : any;
+}
+
 declare global {
     var _TOOL_DATA_ : IToolData;
 }
@@ -96,6 +116,8 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
     private _UICanvas = React.createRef<HTMLCanvasElement>();
     private _2DCanvas = React.createRef<HTMLCanvasElement>();
     private _3DCanvas = React.createRef<HTMLCanvasElement>();
+    private _timer : number | null;
+    private static PREVIEW_UPDATE_DELAY_MS = 160;
 
     constructor(props : ITextureEditorComponentProps) {
         super(props);
@@ -114,17 +136,23 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
             activeToolIndex: -1,
             metadata: {
                 color: '#ffffff',
-                opacity: 1
+                alpha: 1,
+                select: {
+                    x1: -1,
+                    y1: -1,
+                    x2: -1,
+                    y2: -1
+                }
             },
             channels,
             pixelData: {},
-            face: 0
+            face: 0,
+            mipLevel: 0
         }
         this.loadToolFromURL = this.loadToolFromURL.bind(this);
         this.changeTool = this.changeTool.bind(this);
         this.setMetadata = this.setMetadata.bind(this);
         this.saveTexture = this.saveTexture.bind(this);
-        this.setFace = this.setFace.bind(this);
         this.resetTexture = this.resetTexture.bind(this);
         this.resizeTexture = this.resizeTexture.bind(this);
         this.uploadTexture = this.uploadTexture.bind(this);
@@ -138,7 +166,10 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
             this._UICanvas.current!,
             this._2DCanvas.current!,
             this._3DCanvas.current!,
-            (data : IPixelData) => {this.setState({pixelData: data})}
+            (data : IPixelData) => {this.setState({pixelData: data})},
+            this.state.metadata,
+            () => this.textureDidUpdate(),
+            data => this.setMetadata(data)
         );
         this.addTools(defaultTools);
     }
@@ -147,14 +178,24 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
         let channelsClone : IChannel[] = [];
         this.state.channels.forEach(channel => channelsClone.push({...channel}));
         this._textureCanvasManager.channels = channelsClone;
-        this._textureCanvasManager.metadata = {...this.state.metadata};
         this._textureCanvasManager.face = this.state.face;
+        this._textureCanvasManager.mipLevel = this.state.mipLevel;
     }
 
     componentWillUnmount() {
         this._textureCanvasManager.dispose();
     }
 
+    textureDidUpdate() {
+        if (this._timer != null) {
+            clearTimeout(this._timer);
+        }
+        this._timer = window.setTimeout(() => {
+            this.props.onUpdate();
+            this._timer = null;
+        }, TextureEditorComponent.PREVIEW_UPDATE_DELAY_MS);
+    }
+
     loadToolFromURL(url : string) {
         Tools.LoadScript(url, () => {
             this.addTools([_TOOL_DATA_]);
@@ -182,6 +223,9 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
             scene3D: this._textureCanvasManager.scene3D,
             size: this._textureCanvasManager.size,
             updateTexture: () => this._textureCanvasManager.updateTexture(),
+            startPainting: () => this._textureCanvasManager.startPainting(),
+            stopPainting: () => this._textureCanvasManager.stopPainting(),
+            updatePainting: () => this._textureCanvasManager.updatePainting(),
             metadata: this.state.metadata,
             setMetadata: (data : any) => this.setMetadata(data),
             getMouseCoordinates: (pointerInfo : PointerInfo) => this._textureCanvasManager.getMouseCoordinates(pointerInfo),
@@ -205,10 +249,7 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
             ...newMetadata
         }
         this.setState({metadata: data});
-    }
-
-    setFace(face: number) {
-        this.setState({face});
+        this._textureCanvasManager.metadata = data;
     }
 
     saveTexture() {
@@ -236,10 +277,12 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
                 saveTexture={this.saveTexture}
                 pixelData={this.state.pixelData}
                 face={this.state.face}
-                setFace={this.setFace}
+                setFace={face => this.setState({face})}
                 resetTexture={this.resetTexture}
                 resizeTexture={this.resizeTexture}
                 uploadTexture={this.uploadTexture}
+                mipLevel={this.state.mipLevel}
+                setMipLevel={mipLevel => this.setState({mipLevel})}
             />
             {!this.props.texture.isCube && <ToolBar
                 tools={this.state.tools}
@@ -251,7 +294,7 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
             />}
             <ChannelsBar channels={this.state.channels} setChannels={(channels) => {this.setState({channels})}}/>
             <TextureCanvasComponent canvas2D={this._2DCanvas} canvas3D={this._3DCanvas} canvasUI={this._UICanvas} texture={this.props.texture}/>
-            <BottomBar name={this.props.url}/>
+            <BottomBar name={this.props.url} mipLevel={this.state.mipLevel} hasMips={!this.props.texture.noMipmap}/>
         </div>
     }
 }

+ 7 - 5
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/toolBar.tsx

@@ -1,6 +1,6 @@
 import * as React from 'react';
 import { SketchPicker } from 'react-color';
-import { IToolData, IToolType } from './textureEditorComponent';
+import { IToolData, IToolType, IMetadata } from './textureEditorComponent';
 
 export interface ITool extends IToolData {
     instance: IToolType;
@@ -11,7 +11,7 @@ interface IToolBarProps {
     addTool(url: string): void;
     changeTool(toolIndex : number): void;
     activeToolIndex : number;
-    metadata: any;
+    metadata: IMetadata;
     setMetadata(data : any): void;
 }
 
@@ -37,7 +37,7 @@ export class ToolBar extends React.Component<IToolBarProps, IToolBarState> {
     }
 
     computeRGBAColor() {
-        const opacityInt = Math.floor(this.props.metadata.opacity * 255);
+        const opacityInt = Math.floor(this.props.metadata.alpha * 255);
         const opacityHex = opacityInt.toString(16).padStart(2, '0');
         return `${this.props.metadata.color}${opacityHex}`;
     }
@@ -79,7 +79,9 @@ export class ToolBar extends React.Component<IToolBarProps, IToolBarState> {
                 </div>
             </div>
             <div id='color' onClick={() => this.setState({pickerOpen: !this.state.pickerOpen})} title='Color' className={`icon button${this.state.pickerOpen ? ` active` : ``}`}>
-                <div id='activeColor' style={{backgroundColor: this.props.metadata.color}}></div>
+                <div id='active-color-bg'>
+                    <div id='active-color' style={{backgroundColor: this.props.metadata.color, opacity: this.props.metadata.alpha}}></div>
+                </div>
             </div>
             {
                 this.state.pickerOpen &&
@@ -92,7 +94,7 @@ export class ToolBar extends React.Component<IToolBarProps, IToolBarState> {
                     }}>
                     </div>
                     <div className='color-picker' ref={this._pickerRef}>
-                            <SketchPicker color={this.computeRGBAColor()}  onChange={color => this.props.setMetadata({color: color.hex, opacity: color.rgb.a})}/>
+                            <SketchPicker color={this.computeRGBAColor()}  onChange={color => this.props.setMetadata({color: color.hex, alpha: color.rgb.a})}/>
                     </div>
                 </>
             }

+ 15 - 0
inspector/src/lod.ts

@@ -0,0 +1,15 @@
+import { Effect } from "babylonjs/Materials/effect";
+
+let name = 'lodPixelShader';
+let shader = `
+varying vec2 vUV;
+uniform sampler2D textureSampler;
+uniform float lod;
+void main(void)
+{
+gl_FragColor=textureLod(textureSampler,vUV,lod);
+}`;
+
+Effect.ShadersStore[name] = shader;
+/** @hidden */
+export var lodPixelShader = { name, shader };

+ 33 - 0
inspector/src/lodCube.ts

@@ -0,0 +1,33 @@
+import { Effect } from "babylonjs/Materials/effect";
+
+let name = 'lodCubePixelShader';
+let shader = `
+varying vec2 vUV;
+uniform samplerCube textureSampler;
+uniform float lod;
+void main(void)
+{
+vec2 uv=vUV*2.0-1.0;
+#ifdef POSITIVEX
+gl_FragColor=textureCube(textureSampler,vec3(1.001,uv.y,uv.x),lod);
+#endif
+#ifdef NEGATIVEX
+gl_FragColor=textureCube(textureSampler,vec3(-1.001,uv.y,uv.x),lod);
+#endif
+#ifdef POSITIVEY
+gl_FragColor=textureCube(textureSampler,vec3(uv.y,1.001,uv.x),lod);
+#endif
+#ifdef NEGATIVEY
+gl_FragColor=textureCube(textureSampler,vec3(uv.y,-1.001,uv.x),lod);
+#endif
+#ifdef POSITIVEZ
+gl_FragColor=textureCube(textureSampler,vec3(uv,1.001),lod);
+#endif
+#ifdef NEGATIVEZ
+gl_FragColor=textureCube(textureSampler,vec3(uv,-1.001),lod);
+#endif
+}`;
+
+Effect.ShadersStore[name] = shader;
+/** @hidden */
+export var lodCubePixelShader = { name, shader };

+ 28 - 18
inspector/src/textureHelper.ts

@@ -1,12 +1,14 @@
 import { PostProcess } from 'babylonjs/PostProcesses/postProcess';
 import { Texture } from 'babylonjs/Materials/Textures/texture';
-import { PassPostProcess, PassCubePostProcess } from 'babylonjs/PostProcesses/passPostProcess';
-import { Constants } from 'babylonjs/Engines/constants';
 import { GlobalState } from './components/globalState';
 import { RenderTargetTexture } from 'babylonjs/Materials/Textures/renderTargetTexture';
 import { BaseTexture } from 'babylonjs/Materials/Textures/baseTexture';
 import { Nullable } from 'babylonjs/types';
 
+import "./lod";
+import "./lodCube";
+
+
 export interface TextureChannelsToDisplay {
     R: boolean;
     G: boolean;
@@ -16,27 +18,34 @@ export interface TextureChannelsToDisplay {
 
 export class TextureHelper {
 
-    private static _ProcessAsync(texture: BaseTexture, width: number, height: number, face: number, channels: TextureChannelsToDisplay, globalState: Nullable<GlobalState>, resolve: (result: Uint8Array) => void, reject: () => void) {
+    private static _ProcessAsync(texture: BaseTexture, width: number, height: number, face: number, channels: TextureChannelsToDisplay, lod: number, globalState: Nullable<GlobalState>, resolve: (result: Uint8Array) => void, reject: () => void) {
         var scene = texture.getScene()!;
         var engine = scene.getEngine();
 
-        let passPostProcess: PostProcess;
+        let lodPostProcess: PostProcess;
 
         if (!texture.isCube) {
-            passPostProcess = new PassPostProcess("pass", 1, null, Texture.NEAREST_SAMPLINGMODE, engine, false, Constants.TEXTURETYPE_UNSIGNED_INT);
+            lodPostProcess = new PostProcess("lod", "lod", ["lod"], null, 1.0, null, Texture.NEAREST_SAMPLINGMODE, engine);
         } else {
-            var passCubePostProcess = new PassCubePostProcess("pass", 1, null, Texture.NEAREST_SAMPLINGMODE, engine, false, Constants.TEXTURETYPE_UNSIGNED_INT);
-            passCubePostProcess.face = face;
-
-            passPostProcess = passCubePostProcess;
+            const faceDefines = [
+                "#define POSITIVEX",
+                "#define NEGATIVEX",
+                "#define POSITIVEY",
+                "#define NEGATIVEY",
+                "#define POSITIVEZ",
+                "#define NEGATIVEZ",
+            ];
+            lodPostProcess = new PostProcess("lodCube", "lodCube", ["lod"], null, 1.0, null, Texture.NEAREST_SAMPLINGMODE, engine, false, faceDefines[face]);
         }
 
-        if (!passPostProcess.getEffect().isReady()) {
+        
+
+        if (!lodPostProcess.getEffect().isReady()) {
             // Try again later
-            passPostProcess.dispose();
+            lodPostProcess.dispose();
 
             setTimeout(() => {
-                this._ProcessAsync(texture, width, height, face, channels, globalState, resolve, reject);
+                this._ProcessAsync(texture, width, height, face, channels, lod, globalState, resolve, reject);
             }, 250);
 
             return;
@@ -51,14 +60,15 @@ export class TextureHelper {
             { width: width, height: height },
             scene, false);
 
-        passPostProcess.onApply = function(effect) {
+        lodPostProcess.onApply = function(effect) {
             effect.setTexture("textureSampler", texture);
+            effect.setFloat("lod", lod);
         };
 
         let internalTexture = rtt.getInternalTexture();
 
         if (internalTexture) {
-            scene.postProcessManager.directRender([passPostProcess], internalTexture);
+            scene.postProcessManager.directRender([lodPostProcess], internalTexture);
 
             // Read the contents of the framebuffer
             var numberOfChannelsByLine = width * 4;
@@ -141,23 +151,23 @@ export class TextureHelper {
         }
 
         rtt.dispose();
-        passPostProcess.dispose();
+        lodPostProcess.dispose();
         
         if (globalState) {
             globalState.blockMutationUpdates = false;
         }
     }
 
-    public static GetTextureDataAsync(texture: BaseTexture, width: number, height: number, face: number, channels: TextureChannelsToDisplay, globalState?: GlobalState): Promise<Uint8Array> {
+    public static GetTextureDataAsync(texture: BaseTexture, width: number, height: number, face: number, channels: TextureChannelsToDisplay, globalState?: GlobalState, lod: number = 0): Promise<Uint8Array> {
         return new Promise((resolve, reject) => {
             if (!texture.isReady() && texture._texture) {
                 texture._texture.onLoadedObservable.addOnce(() => {
-                    this._ProcessAsync(texture, width, height, face, channels, globalState || null, resolve, reject);
+                    this._ProcessAsync(texture, width, height, face, channels, lod, globalState || null, resolve, reject);
                 });
                 return;
             }        
 
-            this._ProcessAsync(texture, width, height, face, channels, globalState || null, resolve, reject);
+            this._ProcessAsync(texture, width, height, face, channels, lod, globalState || null, resolve, reject);
         });
     }
 }