/*
 * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.scenario.effect.impl.state;

import com.sun.javafx.geom.Rectangle;
import com.sun.javafx.geom.transform.Affine2D;
import com.sun.javafx.geom.transform.BaseTransform;
import com.sun.javafx.geom.transform.NoninvertibleTransformException;
import com.sun.scenario.effect.Color4f;
import com.sun.scenario.effect.Filterable;
import com.sun.scenario.effect.ImageData;
import com.sun.scenario.effect.impl.BufferUtil;
import java.nio.FloatBuffer;

/**
 */
public class GaussianRenderState extends LinearConvolveRenderState {
    public static final float MAX_RADIUS = (MAX_KERNEL_SIZE - 1) / 2;

    // General variables representing the convolve operation
    private boolean isShadow;
    private Color4f shadowColor;
    private float spread;

    // Values specific to this operation, calculated from the rendering context
    private EffectCoordinateSpace space;
    private BaseTransform inputtx;
    private BaseTransform resulttx;
    private float inputRadiusX;  // expected radius given inputtx
    private float inputRadiusY;
    private float spreadPass;

    // Values specific to a given filter pass
    private int validatedPass;
    private PassType passType;
    private float passRadius;   // actual radius for src ImageData
    private FloatBuffer weights;
    private float samplevectors[];  // dx, dy for pixel sampling, both passes
    private float weightsValidRadius;
    private float weightsValidSpread;

    static FloatBuffer getGaussianWeights(FloatBuffer weights,
                                          int pad,
                                          float radius,
                                          float spread)
    {
        int r = pad;
        int klen = (r * 2) + 1;
        if (weights == null) {
            weights = BufferUtil.newFloatBuffer(128);
        }
        weights.clear();
        float sigma = radius / 3;
        float sigma22 = 2 * sigma * sigma;
        if (sigma22 < Float.MIN_VALUE) {
            // Avoid divide by 0 below (it can generate NaN values).
            sigma22 = Float.MIN_VALUE;
        }
        float total = 0.0F;
        for (int row = -r; row <= r; row++) {
            float kval = (float) Math.exp(-(row * row) / sigma22);
            weights.put(kval);
            total += kval;
        }
        total += (weights.get(0) - total) * spread;
        for (int i = 0; i < klen; i++) {
            weights.put(i, weights.get(i) / total);
        }
        int limit = getPeerSize(klen);
        while (weights.position() < limit) {
            weights.put(0.0F);
        }
        weights.limit(limit);
        weights.rewind();
        return weights;
    }

    
Constructs a RenderState for a 2 dimensional Gaussian convolution.
Params:
  • xradius – the Gaussian radius along the user space X axis
  • yradius – the Gaussian radius along the user space Y axis
  • spread – the spread amount
  • isShadow – true if this is a shadow operation
  • shadowColor – the color of the shadow operation
  • filtertx – the transform applied to the filter operation
/** * Constructs a {@link RenderState} for a 2 dimensional Gaussian convolution. * * @param xradius the Gaussian radius along the user space X axis * @param yradius the Gaussian radius along the user space Y axis * @param spread the spread amount * @param isShadow true if this is a shadow operation * @param shadowColor the color of the shadow operation * @param filtertx the transform applied to the filter operation */
public GaussianRenderState(float xradius, float yradius, float spread, boolean isShadow, Color4f shadowColor, BaseTransform filtertx) { /* * The operation starts as a description of the size of a (pair of) * Gaussian kernels measured relative to that user space coordinate * system and to be applied horizontally and vertically in that same * space. The presence of a filter transform can mean that the * direction we apply the gaussian convolutions could change as well * as the new size of that Gaussian distribution curve relative to * the pixels produced under that transform. * * We will track the direction and size of the Gaussian as we traverse * different coordinate spaces with the intent that eventually we * will perform the math of the convolution with weights calculated * for one sample per pixel in the indicated direction and applied as * closely to the intended final filter transform as we can achieve * with the following caveats: * * - There is a maximum kernel size that the hardware pixel shaders * can apply so we will try to keep the scaling of the filtered * pixels low enough that we do not exceed that data limitation. * * - Software prefers to apply these weights along horizontal and * vertical vectors, but can apply them in an arbitrary direction * if need be. * * - If the Gaussian kernel is large enough, then applying a smaller * Gaussian kernel to a downscaled input is indistinguishable to * applying the larger kernel to a larger scaled input. Our maximum * kernel size is large enough for this effect to be hidden if we * max out the kernel. * * - We can tell the inputs what transform we want them to use, but * they can always produce output under a different transform and * then return a result with a "post-processing" trasnform to be * applied (as we are doing here ourselves). Thus, we can plan * how we want to apply the convolution weights and samples here, * but we will have to reevaluate our actions when the actual * input pixels are created later. * * - If we are blurring enough to trigger the MAX_RADIUS exceptions * then we can blur at a nice axis-aligned orientation (which is * preferred for the software versions of the shaders) and perform * any rotation and skewing in the final post-processing result * transform as that amount of blurring will quite effectively cover * up any distortion that would occur by not rendering at the * appropriate angles. * * To achieve this we start out with untransformed sample vectors * which are unit vectors along the X and Y axes. We transform them * into the requested filter space, adjust the kernel size and see * if we can support that kernel size. If it is too large of a * projected kernel, then we request the input at a smaller scale * and perform a maximum kernel convolution on it and then indicate * that this result will need to be scaled by the caller. When this * method is done we will have computed what we need to do to the * input pixels when they come in if the inputtx was honored, otherwise * we may have to adjust the values further in {@link @validateInput()}. */ this.isShadow = isShadow; this.shadowColor = shadowColor; this.spread = spread; if (filtertx == null) filtertx = BaseTransform.IDENTITY_TRANSFORM; double mxx = filtertx.getMxx(); double mxy = filtertx.getMxy(); double myx = filtertx.getMyx(); double myy = filtertx.getMyy(); // Transformed unit axis vectors are essentially (mxx, myx) and (mxy, myy). double txScaleX = Math.hypot(mxx, myx); double txScaleY = Math.hypot(mxy, myy); boolean scaled = false; float scaledRadiusX = (float) (xradius * txScaleX); float scaledRadiusY = (float) (yradius * txScaleY); if (scaledRadiusX < MIN_EFFECT_RADIUS && scaledRadiusY < MIN_EFFECT_RADIUS) { // Entire blur is essentially a NOP in device space, we should // set up the values to force NOP processing rather than relying // on calculations to do it for us. this.inputRadiusX = 0.0f; this.inputRadiusY = 0.0f; this.spreadPass = 0; this.space = EffectCoordinateSpace.RenderSpace; this.inputtx = filtertx; this.resulttx = BaseTransform.IDENTITY_TRANSFORM; this.samplevectors = new float[] { 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f }; } else { if (scaledRadiusX > MAX_RADIUS) { scaledRadiusX = MAX_RADIUS; txScaleX = MAX_RADIUS / xradius; scaled = true; } if (scaledRadiusY > MAX_RADIUS) { scaledRadiusY = MAX_RADIUS; txScaleY = MAX_RADIUS / yradius; scaled = true; } this.inputRadiusX = scaledRadiusX; this.inputRadiusY = scaledRadiusY; // We need to apply the spread on only one pass // Prefer pass1 if r1 is not tiny (or at least bigger than r0) // Otherwise use pass 0 so that it doesn't disappear this.spreadPass = (inputRadiusY > 1f || inputRadiusY >= inputRadiusX) ? 1 : 0; if (scaled) { this.space = EffectCoordinateSpace.CustomSpace; this.inputtx = BaseTransform.getScaleInstance(txScaleX, txScaleY); this.resulttx = filtertx .copy() .deriveWithScale(1.0 / txScaleX, 1.0 / txScaleY, 1.0); // assert resulttx.deriveWithConcatenation(inputtx).equals(filtertx) this.samplevectors = new float[] { 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f }; } else { this.space = EffectCoordinateSpace.RenderSpace; this.inputtx = filtertx; this.resulttx = BaseTransform.IDENTITY_TRANSFORM; // These values should produce 2 normalized unit vectors in the // direction of the transformed axis vectors. this.samplevectors = new float[] { (float) (mxx / txScaleX), (float) (myx / txScaleX), (float) (mxy / txScaleY), (float) (myy / txScaleY), 0.0f, 0.0f }; } } // If the input honors our requested transforms then samplevectors // will be the unit vectors in the correct direction to sample by // pixel distances in the input texture and the inputRadii will be // the correct Gaussian dimension to blur them. }
Constructs a RenderState for a single dimensional, directional Gaussian convolution (as for a MotionBlur operation).
Params:
  • radius – the Gaussian radius along the indicated direction
  • dx – the delta X of the unit vector along which to apply the convolution
  • dy – the delta Y of the unit vector along which to apply the convolution
  • filtertx – the transform applied to the filter operation
/** * Constructs a {@link RenderState} for a single dimensional, directional * Gaussian convolution (as for a MotionBlur operation). * * @param radius the Gaussian radius along the indicated direction * @param dx the delta X of the unit vector along which to apply the convolution * @param dy the delta Y of the unit vector along which to apply the convolution * @param filtertx the transform applied to the filter operation */
public GaussianRenderState(float radius, float dx, float dy, BaseTransform filtertx) { // This is a special case of the above 2 dimensional Gaussian, most of // the same strategies and caveats apply except as relevant to our // directional single-axis peculiarities this.isShadow = false; this.spread = 0.0f; if (filtertx == null) filtertx = BaseTransform.IDENTITY_TRANSFORM; double mxx = filtertx.getMxx(); double mxy = filtertx.getMxy(); double myx = filtertx.getMyx(); double myy = filtertx.getMyy(); // Manually transform the unit vector and determine its added "scale" double tdx = mxx * dx + mxy * dy; double tdy = myx * dx + myy * dy; double txScale = Math.hypot(tdx, tdy); boolean scaled = false; float scaledRadius = (float) (radius * txScale); if (scaledRadius < MIN_EFFECT_RADIUS) { this.inputRadiusX = 0.0f; this.inputRadiusY = 0.0f; this.spreadPass = 0; this.space = EffectCoordinateSpace.RenderSpace; this.inputtx = filtertx; this.resulttx = BaseTransform.IDENTITY_TRANSFORM; this.samplevectors = new float[] { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }; } else { if (scaledRadius > MAX_RADIUS) { scaledRadius = MAX_RADIUS; txScale = MAX_RADIUS / radius; scaled = true; } this.inputRadiusX = scaledRadius; this.inputRadiusY = 0.0f; this.spreadPass = 0; if (scaled) { // Since this is a highly directed blur and any change in // scaling perpendicular to the blur angle could result in // visible artifacts not absorbed by the Gaussian convolution, // we will try to focus any changes in intermediate scaling // on just that direction that the blur is applied along. // We will need to calculate 2 disjoint scale factors, one // along the blur (already calculated in txScale) and one // perpendicular to that vector, then we will provide the // inputs with an animorphically scaled coordinate system // that uses a smaller scale along the direction of the blur // and as close as possible to the original scale in the // orthogonal direction... // Determine the orthogonal scale factor: double odx = mxy * dx - mxx * dy; double ody = myy * dx - myx * dy; double txOScale = Math.hypot(odx, ody); this.space = EffectCoordinateSpace.CustomSpace; Affine2D a2d = new Affine2D(); a2d.scale(txScale, txOScale); a2d.rotate(dx, -dy); BaseTransform a2di; try { a2di = a2d.createInverse(); } catch (NoninvertibleTransformException ex) { a2di = BaseTransform.IDENTITY_TRANSFORM; } this.inputtx = a2d; this.resulttx = filtertx .copy() .deriveWithConcatenation(a2di); // assert resulttx.deriveWithConcatenation(inputtx).equals(filtertx) this.samplevectors = new float[] { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }; } else { this.space = EffectCoordinateSpace.RenderSpace; this.inputtx = filtertx; this.resulttx = BaseTransform.IDENTITY_TRANSFORM; // These values should produce a normalized unit vector in the // direction of the transformed sample vector. this.samplevectors = new float[] { (float) (tdx / txScale), (float) (tdy / txScale), 0.0f, 0.0f, 0.0f, 0.0f }; } } // If the input honors our requested transforms then samplevectors // will be the unit vector in the correct direction to sample by // pixel distances in the input texture and the inputRadiusX will be // the correct Gaussian dimension to blur them. // The second vector in samplevectors is ignored since the associated // inputRadiusY is hard-coded to 0. } @Override public boolean isShadow() { return isShadow; } @Override public Color4f getShadowColor() { return shadowColor; } @Override public float[] getPassShadowColorComponents() { return (validatedPass == 0) ? BLACK_COMPONENTS : shadowColor.getPremultipliedRGBComponents(); } @Override public EffectCoordinateSpace getEffectTransformSpace() { return space; } @Override public BaseTransform getInputTransform(BaseTransform filterTransform) { return inputtx; } @Override public BaseTransform getResultTransform(BaseTransform filterTransform) { return resulttx; } @Override public Rectangle getInputClip(int i, Rectangle filterClip) { if (filterClip != null) { double dx0 = samplevectors[0] * inputRadiusX; double dy0 = samplevectors[1] * inputRadiusX; double dx1 = samplevectors[2] * inputRadiusY; double dy1 = samplevectors[3] * inputRadiusY; int padx = (int) Math.ceil(dx0+dx1); int pady = (int) Math.ceil(dy0+dy1); if ((padx | pady) != 0) { filterClip = new Rectangle(filterClip); filterClip.grow(padx, pady); } } return filterClip; } @Override public ImageData validatePassInput(ImageData src, int pass) { this.validatedPass = pass; Filterable f = src.getUntransformedImage(); BaseTransform srcTx = src.getTransform(); float iRadius = (pass == 0) ? inputRadiusX : inputRadiusY; int vecindex = pass * 2; if (srcTx.isTranslateOrIdentity()) { // The input effect gave us exactly what we wanted, proceed as planned this.passRadius = iRadius; samplevectors[4] = samplevectors[vecindex]; samplevectors[5] = samplevectors[vecindex+1]; if (validatedPass == 0) { if ( nearOne(samplevectors[4], f.getPhysicalWidth()) && nearZero(samplevectors[5], f.getPhysicalWidth())) { passType = PassType.HORIZONTAL_CENTERED; } else { passType = PassType.GENERAL_VECTOR; } } else { if (nearZero(samplevectors[4], f.getPhysicalHeight()) && nearOne(samplevectors[5], f.getPhysicalHeight())) { passType = PassType.VERTICAL_CENTERED; } else { passType = PassType.GENERAL_VECTOR; } } } else { // The input produced a texture that requires transformation, // reevaluate our radii. // First (inverse) transform our sample vectors from the intended // srcTx space back into the actual pixel space of the src texture. // Then evaluate their length and attempt to absorb as much of any // implicit scaling that would happen into our final pixelRadii, // but if we overflow the maximum supportable radius then we will // just have to sample sparsely with a longer than unit vector. // REMIND: we should also downsample the texture by powers of // 2 if our sampling will be more sparse than 1 sample per 2 // pixels. passType = PassType.GENERAL_VECTOR; try { srcTx.inverseDeltaTransform(samplevectors, vecindex, samplevectors, 4, 1); } catch (NoninvertibleTransformException ex) { this.passRadius = 0.0f; samplevectors[4] = samplevectors[5] = 0.0f; return src; } double srcScale = Math.hypot(samplevectors[4], samplevectors[5]); float pRad = (float) (iRadius * srcScale); if (pRad > MAX_RADIUS) { pRad = MAX_RADIUS; srcScale = MAX_RADIUS / iRadius; } this.passRadius = pRad; // For a pixelRadius that was less than MAX_RADIUS, the following // lines renormalize the un-transformed vectors back into unit // vectors in the proper direction and we absorbed their length // into the pixelRadius that we will apply for the Gaussian weights. // If we clipped the pixelRadius to MAX_RADIUS, then they will not // actually end up as unit vectors, but they will represent the // proper sampling deltas for the indicated radius (which should // be MAX_RADIUS in that case). samplevectors[4] /= srcScale; samplevectors[5] /= srcScale; } samplevectors[4] /= f.getPhysicalWidth(); samplevectors[5] /= f.getPhysicalHeight(); return src; } @Override public Rectangle getPassResultBounds(Rectangle srcdimension, Rectangle outputClip) { // Note that the pass vector and the pass radius may be adjusted for // a transformed input, but our output will be in the untransformed // "filter" coordinate space so we need to use the "input" values that // are in that same coordinate space. // The srcdimension is padded by the amount of extra data we produce // for this pass. // The outputClip is padded by the amount of extra input data we will // need for subsequent passes to do their work. double r = (validatedPass == 0) ? inputRadiusX : inputRadiusY; int i = validatedPass * 2; double dx = samplevectors[i+0] * r; double dy = samplevectors[i+1] * r; int padx = (int) Math.ceil(Math.abs(dx)); int pady = (int) Math.ceil(Math.abs(dy)); Rectangle ret = new Rectangle(srcdimension); ret.grow(padx, pady); if (outputClip != null) { if (validatedPass == 0) { // Pass 0 needs to retain any added area for Pass 1 to // compute the bounds within the outputClip, so we expand // the outputClip accordingly. dx = samplevectors[2] * r; dy = samplevectors[3] * r; padx = (int) Math.ceil(Math.abs(dx)); pady = (int) Math.ceil(Math.abs(dy)); if ((padx | pady) != 0) { outputClip = new Rectangle(outputClip); outputClip.grow(padx, pady); } } ret.intersectWith(outputClip); } return ret; } @Override public PassType getPassType() { return passType; } @Override public float[] getPassVector() { float xoff = samplevectors[4]; // / srcNativeBounds.width; float yoff = samplevectors[5]; // / srcNativeBounds.height; int ksize = getPassKernelSize(); int center = ksize / 2; float ret[] = new float[4]; ret[0] = xoff; ret[1] = yoff; ret[2] = -center * xoff; ret[3] = -center * yoff; return ret; } @Override public int getPassWeightsArrayLength() { validateWeights(); return weights.limit() / 4; } @Override public FloatBuffer getPassWeights() { validateWeights(); weights.rewind(); return weights; } @Override public int getInputKernelSize(int pass) { return 1 + 2 * (int) Math.ceil((pass == 0) ? inputRadiusX : inputRadiusY); } @Override public int getPassKernelSize() { return 1 + 2 * (int) Math.ceil(passRadius); } @Override public boolean isNop() { if (isShadow) return false; return inputRadiusX < MIN_EFFECT_RADIUS && inputRadiusY < MIN_EFFECT_RADIUS; } @Override public boolean isPassNop() { if (isShadow && validatedPass == 1) return false; return (passRadius) < MIN_EFFECT_RADIUS; } private void validateWeights() { float r = passRadius; float s = (validatedPass == spreadPass) ? spread : 0f; if (weights == null || weightsValidRadius != r || weightsValidSpread != s) { weights = getGaussianWeights(weights, (int) Math.ceil(r), r, s); weightsValidRadius = r; weightsValidSpread = s; } } }