소스 검색

optimize painting

Darragh Burke 5 년 전
부모
커밋
722f90f564

+ 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: 0;
+                        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 - 1
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/bottomBar.tsx

@@ -9,7 +9,7 @@ interface BottomBarProps {
 export class BottomBar extends React.Component<BottomBarProps> {
     render() {
         return <div id='bottom-bar'>
-            <span id='file-url'>{this.props.name}</span>
+            <span id='file-url'>{this.props.name} {this.props.hasMips ? "true" : "false"}</span>
             {this.props.hasMips && <span id='mip-level'>MIP Preview: {this.props.mipLevel}</span>}
         </div>;
     }

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

@@ -11,13 +11,14 @@ export const Floodfill : IToolData = {
         }
 
         fill() {
-            const {metadata, startPainting, stopPainting} = this.getParameters();
+            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);
-            stopPainting(ctx);
+            updatePainting();
+            stopPainting();
         }
         
         setup () {

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 94 - 46
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/paintbrush.ts


+ 7 - 6
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/defaultTools/rectangleSelect.ts

@@ -8,6 +8,8 @@ export const RectangleSelect : IToolData = {
         getParameters: () => IToolParameters;
         pointerObserver: Nullable<Observer<PointerInfo>>;
         isSelecting = false;
+        xStart : number = -1;
+        yStart : number = -1;
         constructor(getParameters: () => IToolParameters) {
             this.getParameters = getParameters;
         }
@@ -19,7 +21,7 @@ export const RectangleSelect : IToolData = {
                     if (pointerInfo.type === PointerEventTypes.POINTERDOWN) {
                         if (pointerInfo.event.button == 0) {
                             this.isSelecting = true;
-                            const {x, y} = getMouseCoordinates(pointerInfo);
+                            const {x, y} = {x: this.xStart, y: this.yStart} = getMouseCoordinates(pointerInfo);
                             setMetadata({
                                 select: {
                                     x1: x,
@@ -32,13 +34,12 @@ export const RectangleSelect : IToolData = {
                     }
                     if (pointerInfo.type === PointerEventTypes.POINTERMOVE && this.isSelecting) {
                         const {x, y} = getMouseCoordinates(pointerInfo);
-                        const {select} = metadata;
                         setMetadata({
                             select: {
-                                x1: Math.min(x, select.x1),
-                                y1: Math.min(y, select.y1),
-                                x2: Math.max(x, select.x1),
-                                y2: Math.max(y, select.y1)
+                                x1: Math.min(x, this.xStart),
+                                y1: Math.min(y, this.yStart),
+                                x2: Math.max(x, this.xStart),
+                                y2: Math.max(y, this.yStart)
                             }
                         })
                     }

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

@@ -22,7 +22,7 @@ interface IPropertiesBarState {
 
 interface IPixelDataProps {
     name : string;
-    data?: number;
+    data: number | undefined;
 }
 
 export class PropertiesBar extends React.Component<IPropertiesBarProps,IPropertiesBarState> {
@@ -56,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) {
@@ -75,73 +75,73 @@ export class PropertiesBar extends React.Component<IPropertiesBarProps,IProperti
                 <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={() => 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='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)}
-                        />)}
+                        {!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>
-                }
-                {!texture.noMipmap &&
-                    <div className='tab' id='mip-tab'>
-                        <img title='Mip Preview Up' className='icon button' src={this._mipUp} onClick={() => mipLevel > 1 && setMipLevel(mipLevel - 1)} />
-                        <img title='Mip Preview Down' className='icon button' src={this._mipDown} onClick={() => mipLevel < 12 && setMipLevel(mipLevel + 1)} />
+                    <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={() => 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='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={() => 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>;
     }

+ 112 - 78
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureCanvasManager.ts

@@ -83,7 +83,7 @@ export class TextureCanvasManager {
 
     private _channels : IChannel[] = [];
     private _face : number = 0;
-    private _mipLevel : number = 1;
+    private _mipLevel : number = 0;
 
     /* The texture from the original engine that we invoked the editor on */
     private _originalTexture: BaseTexture;
@@ -129,6 +129,11 @@ export class TextureCanvasManager {
     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,
@@ -145,6 +150,7 @@ export class TextureCanvasManager {
         this._UICanvas = canvasUI;
         this._2DCanvas = canvas2D;
         this._3DCanvas = canvas3D;
+        this._paintCanvas = document.createElement('canvas');
         this._setPixelData = setPixelData;
         this._metadata = metadata;
         this._onUpdate = onUpdate;
@@ -161,7 +167,7 @@ export class TextureCanvasManager {
         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);
@@ -172,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',
@@ -219,12 +223,14 @@ export class TextureCanvasManager {
                     uniform int h;
 
                     uniform int time;
-                    uniform int mipLevel;
             
                     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); 
@@ -232,7 +238,7 @@ export class TextureCanvasManager {
                             pattern = 0.7;
                         }
                         vec4 bg = vec4(pattern, pattern, pattern, 1.0);
-                        vec4 col = textureLod(textureSampler, vUV, 6.0);
+                        vec4 col = texture(textureSampler, vUV);
                         if (!r && !g && !b) {
                             if (a) {
                                 col = vec4(col.a, col.a, col.a, 1.0);
@@ -275,32 +281,24 @@ export class TextureCanvasManager {
                         float hF = float(h);
                         int xPixel = int(floor(vUV.x * wF));
                         int yPixel = int(floor((1.0 - vUV.y) * hF));
-                        int xPixe= int(gl_FragCoord.y);
                         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 t = mod(float(time), 500.0);
-                                // float amt = mod(floor((gl_FragCoord.x - gl_FragCoord.y + t) * 0.1), 2.0);
-                                gl_FragColor = vec4(1.0,1.0,1.0,1.0);
+                                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;
                             }
                         }
-                        // if (xPixel >= x1 && yPixel >= y1 && xPixel <= x2 && yPixel <= y2) {
-                        //     if (xPixel == x1 || yPixel == y1 || xPixel == x2 || yPixel == y2) {
-                        //         float dots = mod(gl_FragCoord.x + gl_FragCoord.y, 2.0); 
-                        //         if (dots == 0.0) {
-                        //             gl_FragColor = vec4(0.0,0.0,0.0,1.0);
-                        //         }
-                        //     } else {
-                        //         gl_FragColor = gl_FragColor * 0.8 + vec4(0.0,0.0,1.0,1.0) * 0.2;
-                        //     }
-                        // }
                     }`
             },
         {
             attributes: ['position', 'uv'],
             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);
@@ -314,7 +312,6 @@ export class TextureCanvasManager {
         this._planeMaterial.setInt('w', this._size.width);
         this._planeMaterial.setInt('h', this._size.height);
         this._planeMaterial.setInt('time', 0);
-        this._planeMaterial.setInt('mipLevel', 1);
         this._plane.material = this._planeMaterial;
         
         const adt = AdvancedDynamicTexture.CreateFullscreenUI('gui', true, this._scene);
@@ -398,11 +395,6 @@ export class TextureCanvasManager {
             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();
-            let cursor = 'initial';
-            if (this._tool) {
-                cursor = `url(data:image/svg+xml;base64,${this._tool.icon})`;
-            }
-            this._UICanvas.parentElement!.style.cursor = cursor;
             this._planeMaterial.setInt('time', new Date().getTime());
         });
 
@@ -447,9 +439,8 @@ export class TextureCanvasManager {
                     }
                     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({});
                     }
@@ -477,6 +468,7 @@ export class TextureCanvasManager {
         });
     }
 
+
     public async updateTexture() {
         this._didEdit = true;
         const element = this._editing3D ? this._3DCanvas : this._2DCanvas;
@@ -500,7 +492,8 @@ export class TextureCanvasManager {
             } else {
                 (this._target as HtmlElementTexture).element = element;
             }
-            (this._target as HtmlElementTexture).update((this._originalTexture as Texture).invertY);
+            this.queueTextureUpdate();
+            //(this._target as HtmlElementTexture).update((this._originalTexture as Texture).invertY);
         }
         this._originalTexture._texture = this._target._texture;
         this._channelsTexture.element = element;
@@ -508,54 +501,94 @@ export class TextureCanvasManager {
         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 {
-        if (this._metadata.select.x1 == -1) {
-            return this._2DCanvas.getContext('2d')!;
+        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;
         }
-        const canvas = document.createElement('canvas');
-        canvas.width = this._metadata.select.x2 - this._metadata.select.x1;
-        canvas.height = this._metadata.select.y2 - this._metadata.select.y1;
-        const ctx = canvas.getContext('2d')!;
-        ctx.putImageData(this._2DCanvas.getContext('2d')!.getImageData(this._metadata.select.x1, this._metadata.select.y1, canvas.width, canvas.height), 0, 0);
+        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 stopPainting(ctx: CanvasRenderingContext2D, x1?: number, y1?: number, x2?: number, y2?: number) : void {
-        if (this._metadata.select.x1 == -1) {
-            
-        } else {
-            
-            const pixelData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
-            let editingAllChannels = true;
-            this._channels.forEach(channel => {
-                if (!channel.editable) editingAllChannels = false;
-            })
-            if (!editingAllChannels) {
-                const xStart = this._metadata.select.x1;
-                const yStart = this._metadata.select.y1;
-                const oldData = this._2DCanvas.getContext('2d')!.getImageData(xStart, yStart, ctx.canvas.width, ctx.canvas.height);
-                for(let x = 0; x < pixelData.width; x += 1) {
-                    for(let y = 0; y < pixelData.height; y += 1) {
-                        const i = (x + y * pixelData.width) * 4;
-                        this._channels.forEach((channel, index) => {
-                            if (!channel.editable) {
-                                pixelData.data[i + index] = oldData.data[i + index];
-                            }
-                        })
+    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];
                     }
                 }
-            }
-            ctx.globalAlpha = 1.0;
-            ctx.globalCompositeOperation = 'source-over';
-            this.canvas2D.getContext('2d')?.putImageData(pixelData, this._metadata.select.x1, this._metadata.select.y1);
-            ctx.canvas.parentNode?.removeChild(ctx.canvas);
+            });
+            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._channelsTexture.update(true);
     }
 
     public set channels(channels: IChannel[]) {
@@ -595,8 +628,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();
@@ -661,8 +697,9 @@ export class TextureCanvasManager {
     }
 
     public set mipLevel(mipLevel : number) {
+        if (this._mipLevel === mipLevel) return;
         this._mipLevel = mipLevel;
-        this._planeMaterial.setInt('mipLevel', mipLevel);
+        this.grabOriginalTexture(false);
     }
 
     /** Returns the tool GUI object, allowing tools to access the GUI */
@@ -678,16 +715,10 @@ export class TextureCanvasManager {
     public set metadata(metadata: IMetadata) {
         this._metadata = metadata;
         const {x1,y1,x2,y2} = metadata.select;
-        // x1 = x1/this._size.width * this._UICanvas.width;
-        // y1 = 1-(y1/this._size.height) * this._UICanvas.height;
-        // x2 = x2/this._size.width * this._UICanvas.width;
-        // y2 = (1-y2/this._size.height) * this._UICanvas.height;
-
         this._planeMaterial.setInt('x1', x1);
         this._planeMaterial.setInt('y1', y1);
         this._planeMaterial.setInt('x2', x2);
         this._planeMaterial.setInt('y2', y2);
-        console.log(this._planeMaterial.getEffect()?.getUniformNames());
     }
 
     private makePlane() {
@@ -726,6 +757,8 @@ 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._cameraPos.x = 0;
             this._cameraPos.y = 0;
@@ -766,7 +799,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) => {
@@ -792,7 +825,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();

+ 38 - 42
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureEditor.scss

@@ -51,65 +51,55 @@
         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;
@@ -249,8 +239,14 @@
         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;
         }
     }
 }

+ 11 - 5
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureEditorComponent.tsx

@@ -40,7 +40,7 @@ interface ITextureEditorComponentState {
 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;
@@ -58,8 +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;
-    stopPainting: (ctx: CanvasRenderingContext2D) => void;
+    /** 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;
 }
 
 
@@ -113,6 +117,7 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
     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);
@@ -142,7 +147,7 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
             channels,
             pixelData: {},
             face: 0,
-            mipLevel: 1
+            mipLevel: 0
         }
         this.loadToolFromURL = this.loadToolFromURL.bind(this);
         this.changeTool = this.changeTool.bind(this);
@@ -188,7 +193,7 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
         this._timer = window.setTimeout(() => {
             this.props.onUpdate();
             this._timer = null;
-        }, 300);
+        }, TextureEditorComponent.PREVIEW_UPDATE_DELAY_MS);
     }
 
     loadToolFromURL(url : string) {
@@ -219,7 +224,8 @@ export class TextureEditorComponent extends React.Component<ITextureEditorCompon
             size: this._textureCanvasManager.size,
             updateTexture: () => this._textureCanvasManager.updateTexture(),
             startPainting: () => this._textureCanvasManager.startPainting(),
-            stopPainting: (ctx : CanvasRenderingContext2D) => this._textureCanvasManager.stopPainting(ctx),
+            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),

+ 9 - 0
inspector/src/lod.fragment.fx

@@ -0,0 +1,9 @@
+// Samplers
+varying vec2 vUV;
+uniform sampler2D textureSampler;
+uniform float lod;
+
+void main(void) 
+{
+	gl_FragColor = textureLod(textureSampler, vUV, lod);
+}

+ 28 - 0
inspector/src/lodCube.fragment.fx

@@ -0,0 +1,28 @@
+// Samplers
+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
+}

+ 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.fragment";
+import "./lodCube.fragment";
+
+
 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);
         });
     }
 }