import java.io.File; import java.nio.IntBuffer; import java.util.ArrayList; import java.util.List; import javafx.application.Application; import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.control.Slider; import javafx.scene.effect.BoxBlur; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.image.PixelFormat; import javafx.scene.image.PixelWriter; import javafx.scene.image.WritableImage; import javafx.scene.image.WritablePixelFormat; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.scene.transform.Scale; import javafx.stage.FileChooser; import javafx.stage.Stage; public class ManualMipmapTest4 extends Application { static FileChooser.ExtensionFilter imgfilter = new FileChooser.ExtensionFilter("Img", ".jpg", ".gif", ".jpeg", ".png"); static WritablePixelFormat ARGBPre = PixelFormat.getIntArgbPreInstance(); List scaledImages = new ArrayList<>(); ImageView normalView = new ImageView(); ImageView blurScaledView = new ImageView(); ImageView mipmapView = new ImageView(); ImageView lmipmapView1 = new ImageView(), lmipmapView2 = new ImageView(); ImageView customScaleView = new ImageView(); ImageView areaScaleView = new ImageView(); GridPane allViewsPane; Label scaleLabel; double scale = 1.0; int imgw, imgh; int imgpixels[]; @Override public void start(Stage stage) { Button loadButton = new Button("Load Image"); loadButton.addEventHandler(ActionEvent.ACTION, e -> { FileChooser dlg = new FileChooser(); dlg.setSelectedExtensionFilter(imgfilter); File file = dlg.showOpenDialog(stage.getScene().getWindow()); Image img = new Image(file.toURI().toString()); setImage(img); }); Slider scaleSlider = new Slider(0.0, 1.0, 1.0); scaleSlider.setShowTickLabels(true); scaleSlider.setBlockIncrement(1.0/256.0); scaleSlider.valueProperty().addListener((ObservableValue observable, Number oldValue, Number newValue) -> { setScale(newValue.doubleValue()); }); scaleLabel = new Label("Scale = 1.0"); allViewsPane = new GridPane(); addViewBox(0, 0, "Normal (bilinear from base image) scaling", normalView); addViewBox(1, 0, "BoxBlur scaling", blurScaledView); addViewBox(0, 1, "Simple Mipmap (bilinear from nearest map) scaling", mipmapView); addViewBox(1, 1, "Trilinear Mipmap (bilinear blend from 2 nearest maps) scaling", lmipmapView2, lmipmapView1); // addViewBox(2, 0, "Custom scaling", customScaleView); addViewBox(2, 1, "Area Averaging scaling", areaScaleView); HBox controlsHolder = new HBox(loadButton, scaleSlider, scaleLabel); VBox root = new VBox(allViewsPane, controlsHolder); stage.setScene(new Scene(root, 1200, 900)); stage.show(); setSyntheticImage(); } void addViewBox(int col, int row, String label, ImageView... views) { ScrollPane newPane = new ScrollPane(new Group(views)); Label newLabel = new Label(label); newPane.setPrefSize(10000, 10000); if (allViewsPane.getChildren().size() > 0) { VBox masterBox = (VBox) allViewsPane.getChildren().get(0); ScrollPane masterPane = (ScrollPane) masterBox.getChildren().get(0); newPane.hvalueProperty().bindBidirectional(masterPane.hvalueProperty()); newPane.vvalueProperty().bindBidirectional(masterPane.vvalueProperty()); } allViewsPane.add(new VBox(newPane, newLabel), col, row); } boolean isOn(int v) { if ((v & 7) == 0) return true; if (((v+1) & 31) < 2) return true; return (((v+2) & 127) < 4); } void setSyntheticImage() { int dim = 0xffe; WritableImage wimg = new WritableImage(dim, dim); PixelWriter pw = wimg.getPixelWriter(); for (int y = 0; y < dim; y++) { for (int x = 0; x < dim; x++) { pw.setColor(x, y, isOn(x) || isOn(y) ? Color.BLACK : Color.WHITE); } } setImage(wimg); } void setImage(Image img) { imgw = (int) img.getWidth(); imgh = (int) img.getHeight(); imgpixels = new int[(imgw + 1) * (imgh + 1)]; img.getPixelReader().getPixels(0, 0, imgw, imgh, ARGBPre, imgpixels, 0, imgw+1); int imgoff = imgw; for (int y = 0; y < imgh; y++) { imgpixels[imgoff] = imgpixels[imgoff-1]; imgoff += imgw + 1; } imgoff = imgh * (imgw + 1); for (int x = 0; x <= imgw; x++) { imgpixels[imgoff] = imgpixels[imgoff - imgw - 1]; } normalView.setImage(img); blurScaledView.setImage(img); customScaleView.setImage(img); // blurScaledView.setClip(new Rectangle(img.getWidth(), img.getHeight())); scaledImages.clear(); scaledImages.add(img); ImageView tmpview = new ImageView(); tmpview.getTransforms().setAll(new Scale(.5, .5, 0, 0)); while (scaledImages.get(scaledImages.size()-1).getWidth() > 2.0) { tmpview.setImage(scaledImages.get(scaledImages.size()-1)); // tmpview.setClip(new Rectangle(tmpview.getImage().getWidth(), tmpview.getImage().getHeight())); scaledImages.add(tmpview.snapshot(null, null)); } setScale(scale); } void setScale(double scale) { this.scale = scale; scaleLabel.setText("Scale = " + scale); normalView.getTransforms().setAll(new Scale(scale, scale, 0, 0)); blurScaledView.getTransforms().setAll(new Scale(scale, scale, 0, 0)); int selectorNum; double powScale; if (scale < 1.0) { powScale = Math.abs(Math.log(scale) / Math.log(2.0)); selectorNum = (int) Math.floor(powScale); if (selectorNum >= scaledImages.size()) { selectorNum = scaledImages.size() - 1; } } else { powScale = 0; selectorNum = 0; } double blurDistance = scale < 1.0? powScale * 2.0 + 1.0 : 0; blurScaledView.setEffect(new BoxBlur(blurDistance, blurDistance, 1)); mipmapView.setImage(scaledImages.get(selectorNum)); lmipmapView1.setImage(scaledImages.get(selectorNum)); int selectorNum2 = selectorNum + 1; if (selectorNum2 >= scaledImages.size()) { selectorNum2 = selectorNum; } lmipmapView2.setImage(scaledImages.get(selectorNum2)); double mipmapScale = Math.scalb(scale, selectorNum); mipmapView.getTransforms().setAll(new Scale(mipmapScale, mipmapScale, 0, 0)); lmipmapView1.getTransforms().setAll(new Scale(mipmapScale, mipmapScale, 0, 0)); double mipmapScale2 = Math.scalb(scale, selectorNum2); lmipmapView2.getTransforms().setAll(new Scale(mipmapScale2, mipmapScale2, 0, 0)); lmipmapView1.setOpacity(mipmapScale * 2 - 1); System.out.println("scaling "+scale+" uses blur of "+blurDistance+" and mip image "+selectorNum+" scaled by "+mipmapScale); // customScaleView.setImage(customScaleImage(scale)); areaScaleView.setImage(areaScaleImage(scale)); } static int argb(double accum[], double scale) { int a = (int) (accum[0] * scale); int r = (int) (accum[1] * scale); int g = (int) (accum[2] * scale); int b = (int) (accum[3] * scale); return (a << 24) | (r << 16) | (g << 8) | b; } // static int clamp255(double v) { // int iv = (int) v; // if (iv < 0) return 0; // else if (iv > 255) return 255; // else return (int) v; // } WritableImage customScaleImage(double scale) { int sw = (int) Math.ceil(imgw * scale); int sh = (int) Math.ceil(imgh * scale); WritableImage wimg = new WritableImage(sw, sh); double kw = Math.min(1.0 / scale, imgw); double kh = Math.min(1.0 / scale, imgh); PixelWriter pw = wimg.getPixelWriter(); double accum[] = new double[4]; for (int y = 0; y < sh; y++) { double sy = (y + 0.5) / scale; for (int x = 0; x < sw; x++) { double sx = (x + 0.5) / scale; int samples = 0; accum[0] = accum[1] = accum[2] = accum[3] = 0.0; // System.out.println("sampling "+sx+", "+sy+", for "+x+", "+y+", with kernel: "+kw+", "+kh); for (double sampy = kh / 2.0 - 0.5; sampy >= 0.0; sampy--) { for (double sampx = kw / 2.0 - 0.5; sampx >= 0.0; sampx--) { accum(sx - sampx, sy - sampy, accum); accum(sx + sampx, sy - sampy, accum); accum(sx - sampx, sy + sampy, accum); accum(sx + sampx, sy + sampy, accum); samples += 4; } } pw.setArgb(x, y, argb(accum, 1.0 / samples)); } } return wimg; } void accum(double dx, double dy, double accum[]) { dx -= 0.5; dy -= 0.5; int x0 = (int) dx; int y0 = (int) dy; if (x0 >= imgw) x0 = imgw - 1; if (y0 >= imgh) y0 = imgh - 1; double fractx = dx - x0; double fracty = dy - y0; double fract = fractx * fracty; int off = y0 * (imgw + 1) + x0; if (off+imgw+2 >= imgpixels.length) { System.out.println("sampling for "+(dx+0.5)+", "+(dy+0.5)+" out of "+imgw+", "+imgh); System.out.println(" fetching from "+x0+", "+y0+" with fractions: "+fractx+", "+fracty); } accum(imgpixels[off+imgw+2], accum, fract); accum(imgpixels[off+imgw+1], accum, fracty - fract); accum(imgpixels[off +1], accum, fractx - fract); accum(imgpixels[off ], accum, 1.0 - fractx - fracty + fract); } static void accum(int argb, double accum[], double fract) { accum[0] += ((argb >> 24) & 0xff) * fract; accum[1] += ((argb >> 16) & 0xff) * fract; accum[2] += ((argb >> 8) & 0xff) * fract; accum[3] += ((argb ) & 0xff) * fract; } static double fract(int i0, int i1, double d0, double d1, int i) { if (i == i0) return 1.0 - (d0 - i0); if (i == i1) return d1 - i1; return 1.0; } WritableImage areaScaleImage(double scale) { int sw = (int) Math.ceil(imgw * scale); int sh = (int) Math.ceil(imgh * scale); WritableImage wimg = new WritableImage(sw, sh); PixelWriter pw = wimg.getPixelWriter(); double accum[] = new double[4]; double scalesq = scale * scale; for (int y = 0; y < sw; y++) { double y0 = y / scale; double y1 = (y+1) / scale; if (y1 > imgh) y1 = imgh; int iy0 = (int) Math.floor(y0); int iy1 = (int) Math.floor(y1); if (iy1 == y1) iy1--; for (int x = 0; x < sh; x++) { double x0 = x / scale; double x1 = (x+1) / scale; if (x1 > imgw) x1 = imgw; int ix0 = (int) Math.floor(x0); int ix1 = (int) Math.floor(x1); if (ix1 == x1) ix1--; accum[0] = accum[1] = accum[2] = accum[3] = 0.0; for (int iy = iy0; iy <= iy1; iy++) { double yfract = fract(iy0, iy1, y0, y1, iy); int rowoff = iy * (imgw + 1); for (int ix = ix0; ix <= ix1; ix++) { double xfract = fract(ix0, ix1, x0, x1, ix); accum(imgpixels[rowoff + ix], accum, xfract * yfract); } } pw.setArgb(x, y, argb(accum, scalesq)); } } return wimg; } }