I want to show 3D embossed look and feel as shown in following image. I used EmbossMaskFilter but cannot get it to show the effect (see code below). Is there a different way to
I think you will need a larger blur radius and smaller values for ambient light and specular highlight.
I played around with this by creating a UI to tweak parameters.
Here is the code I used, so you can try it out. I used a simple blue rectangle, but you should be able to easily plug in whatever image you want to draw.
MainActivity.java:
import android.graphics.EmbossMaskFilter;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.SeekBar;
import android.widget.TextView;
public class MainActivity extends ActionBarActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final DrawView drawView = (DrawView) findViewById(R.id.drawView);
final TextView ambientLightTextView = (TextView) findViewById(R.id.ambientLightTextView);
SeekBar ambientLightSeekBar = (SeekBar) findViewById(R.id.ambientLightSeekBar);
ambientLightSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
float value = (float) progress / 1000F;
ambientLightTextView.setText(Float.toString(value));
drawView.setAmbientLight(value);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {}
});
final TextView specularHighlightTextView = (TextView) findViewById(R.id.specularHighlightTextView);
SeekBar specularHighlightSeekBar = (SeekBar) findViewById(R.id.specularHighlightSeekBar);
specularHighlightSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
float value = (float) progress / 100f;
specularHighlightTextView.setText(Float.toString(value));
drawView.setSpecularHighlight(value);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {}
});
final TextView blurRadiusTextView = (TextView) findViewById(R.id.blurRadiusTextView);
SeekBar blurRadiusSeekBar = (SeekBar) findViewById(R.id.blurRadiusSeekBar);
blurRadiusSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
float value = (float) progress / 5F;
blurRadiusTextView.setText(Float.toString(value));
drawView.setBlurRadius(value);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {}
});
}
}
DrawView.java:
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.EmbossMaskFilter;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
public class DrawView extends View {
private Rect rect = new Rect();
private Paint paint;
private EmbossMaskFilter filter;
private float[] mLightSource = new float[] {2, 5, 5};
private float mAmbientLight = 0.5F;
private float mSpecularHighlight = 8F;
private float mBlurRadius = 15f;
public DrawView(Context context) {
super(context);
init();
}
public DrawView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public DrawView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setLayerType(LAYER_TYPE_SOFTWARE, null);
filter = new EmbossMaskFilter(mLightSource, mAmbientLight, mSpecularHighlight, mBlurRadius);
paint = new Paint();
paint.setColor(0xFF0000FF);
paint.setStyle(Paint.Style.FILL);
paint.setAntiAlias(true);
paint.setDither(true);
paint.setMaskFilter(filter);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
rect.left = getWidth() / 3;
rect.right = rect.left * 2;
rect.top = getHeight() / 3;
rect.bottom = rect.top * 2;
canvas.drawRect(rect, paint);
}
public void setAmbientLight(float value) {
if (value > 1.0F) value = 1.0F;
if (value < 0) value = 0;
mAmbientLight = value;
filter = new EmbossMaskFilter(mLightSource, mAmbientLight, mSpecularHighlight, mBlurRadius);
paint.setMaskFilter(filter);
invalidate();
}
public void setSpecularHighlight(float value) {
mSpecularHighlight = value;
filter = new EmbossMaskFilter(mLightSource, mAmbientLight, mSpecularHighlight, mBlurRadius);
paint.setMaskFilter(filter);
invalidate();
}
public void setBlurRadius(float value) {
mBlurRadius = value;
filter = new EmbossMaskFilter(mLightSource, mAmbientLight, mSpecularHighlight, mBlurRadius);
paint.setMaskFilter(filter);
invalidate();
}
}
activity_main.xml:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity">
<!-- make sure you match this up with the actual package name in your code -->
<com.example.embossmaskfilter.DrawView
android:id="@+id/drawView"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layerType="software" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@id/drawView"
android:layout_centerVertical="true"
android:text="Ambient Light"
android:textAppearance="?android:attr/textAppearanceSmall" />
<TextView
android:id="@+id/ambientLightTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignTop="@id/textView2"
android:text="0.5"
android:textAppearance="?android:attr/textAppearanceSmall" />
<SeekBar
android:id="@+id/ambientLightSeekBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/textView2"
android:max="1000" />
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@id/ambientLightSeekBar"
android:layout_centerVertical="true"
android:text="Specular Highlight"
android:textAppearance="?android:attr/textAppearanceSmall" />
<TextView
android:id="@+id/specularHighlightTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignTop="@id/textView3"
android:text="0.5"
android:textAppearance="?android:attr/textAppearanceSmall" />
<SeekBar
android:id="@+id/specularHighlightSeekBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/textView3"
android:max="1000" />
<TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@id/specularHighlightSeekBar"
android:layout_centerVertical="true"
android:text="Blur Radius"
android:textAppearance="?android:attr/textAppearanceSmall" />
<TextView
android:id="@+id/blurRadiusTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignTop="@id/textView4"
android:text="0.5"
android:textAppearance="?android:attr/textAppearanceSmall" />
<SeekBar
android:id="@+id/blurRadiusSeekBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/textView4"
android:max="1000" />
</RelativeLayout>
If you only want to do bitmap processing (as opposed to 3D or vectors), your best bet probably is to:
UPDATE: here comes the code. I tried to reuse your variable names so that it's easier to understand. The code uses Renderscript intrinsics whenever possible in order to make things faster and more interesting.
private Paint fillPaint = null;
private Path path2;
private Bitmap mBitmapIn;
private Bitmap mBitmapPuzzle;
private RenderScript mRS;
private Allocation mInAllocation;
private Allocation mPuzzleAllocation;
private Allocation mCutterAllocation;
private Allocation mOutAllocation;
private Allocation mOutAllocation2;
private Allocation mAllocationHist;
private ScriptIntrinsicBlur mScriptBlur;
private ScriptIntrinsicBlend mScriptBlend;
private ScriptIntrinsicHistogram mScriptHistogram;
private ScriptIntrinsicLUT mScriptLUT;
private Context ctx;
private int bw = 780;
private int bh = 780;
private void init()
{
mBitmapIn = loadBitmap(R.drawable.cat7); // background image
mBitmapPuzzle = Bitmap.createBitmap(bw, bh, Bitmap.Config.ARGB_8888); // this will hold the puzzle
Canvas c = new Canvas(mBitmapPuzzle);
path2 = new Path();
createPath(5); // create the path with stroke width of 5 pixels
c.drawPath(path2, fillPaint); // draw it on canvas
createScript(); // get renderscripts and Allocations ready
// Apply gaussian blur of radius 25 to our drawing
mScriptBlur.setRadius(25);
mScriptBlur.setInput(mPuzzleAllocation);
mScriptBlur.forEach(mOutAllocation);
// Now apply the blur of radius 1
mScriptBlur.setRadius(1);
mScriptBlur.setInput(mPuzzleAllocation);
mScriptBlur.forEach(mOutAllocation2);
// Subtract one blur result from another
mScriptBlend.forEachSubtract(mOutAllocation, mOutAllocation2);
// We now want to normalize the result (e.g. make it use full 0-255 range).
// To do that, we will first compute the histogram of our image
mScriptHistogram.setOutput(mAllocationHist);
mScriptHistogram.forEach(mOutAllocation2);
// copy the histogram to Java array...
int []hist = new int[256 * 4];
mAllocationHist.copyTo(hist);
// ...and walk it from the end looking for the first non empty bin
int i;
for(i = 255; i > 1; i--)
if((hist[i * 4] | hist[i * 4 + 1] | hist[i * 4 + 2]) != 0)
break;
// Now setup the LUTs that will map the image to the new, wider range.
// We also use the opportunity to inverse the image ("255 -").
for(int x = 0; x <= i; x++)
{
int val = 255 - x * 255 / i;
mScriptLUT.setAlpha(x, 255); // note we always make it fully opaque
mScriptLUT.setRed(x, val);
mScriptLUT.setGreen(x, val);
mScriptLUT.setBlue(x, val);
}
// the mapping itself.
mScriptLUT.forEach(mOutAllocation2, mOutAllocation);
Let's make a short break and see what we have so far. Observe that the entire image on the left is opaque (i.e. including the space outside the puzzle), and we now have to cut out the shape and antialias its edge properly. Unfortunately, using original shape won't work, as it is too large and cuts out too much, leading to unpleasant artifacts near the edge (figure on the right).
We therefore draw another path, this time using a narrower stroke...
Bitmap mBitmapCutter = Bitmap.createBitmap(bw, bh, Bitmap.Config.ARGB_8888);
c = new Canvas(mBitmapCutter);
path2 = new Path();
createPath(1); // stroke width 1
c.drawPath(path2, fillPaint);
mCutterAllocation = Allocation.createFromBitmap(mRS, mBitmapCutter);
// cookie cutter now
mScriptBlend.forEachDstIn(mCutterAllocation, mOutAllocation);
...for a much better looking result. Let's use it to mask out a background image.
mScriptBlend.forEachMultiply(mOutAllocation, mInAllocation);
mInAllocation.copyTo(mBitmapPuzzle);
}
Hello there! Now just the Renderscript setup code.
private void createScript() {
mRS = RenderScript.create(ctx);
mPuzzleAllocation = Allocation.createFromBitmap(mRS, mBitmapPuzzle);
// three following allocations could actually use createSized(),
// but the code would be longer.
mInAllocation = Allocation.createFromBitmap(mRS, mBitmapIn);
mOutAllocation = Allocation.createFromBitmap(mRS, mBitmapPuzzle);
mOutAllocation2 = Allocation.createFromBitmap(mRS, mBitmapPuzzle);
mAllocationHist = Allocation.createSized(mRS, Element.I32_3(mRS), 256);
mScriptBlur = ScriptIntrinsicBlur.create(mRS, Element.U8_4(mRS));
mScriptBlend = ScriptIntrinsicBlend.create(mRS, Element.U8_4(mRS));
mScriptHistogram = ScriptIntrinsicHistogram.create(mRS, Element.U8_4(mRS));
mScriptLUT = ScriptIntrinsicLUT.create(mRS, Element.U8_4(mRS));
}
And finally onDraw()
:
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(mBitmapPuzzle, 0, 0, fillPaint);
super.onDraw(canvas);
}
TODO: check if other stroke miters would give more pleasant corners.
If you're not afraid of dabbling into some low-level image processing, you can use convolution matrices to achieve an emboss effect. You could use your image's silhouette (i.e. the alpha channel) as an input to the filter. For example, if you use this 5x5 matrix:
| -2 0 0 0 0 |
| 0 -1 0 0 0 |
| 0 0 n 0 0 | * (1/n)
| 0 0 0 1 0 |
| 0 0 0 0 2 |
and apply it to such image (representing an alpha channel):
you'll get this effect:
All of the computed values have been decreased by 127 to ensure that they will be in range of 0-255. I used n = 10
in this particular example. You can manipulate the radius by using a matrix of different size (it's not hard to extrapolate) and the depth by adjusting the value of n
(the bigger the value, the subtler the effect).
Given the original alpha channel, and the computed mask you can determine offsets to apply to the respective pixels of the original image, thus creating an emboss effect.
Hope that helps.