package com.exh.bevel; import java.util.LinkedList; import java.util.List; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import com.exh.bevel.support.QuadEntry; import com.exh.bevel.support.QuadList; import com.exh.bevel.support.RayIntersectsSurface; import com.exh.bevel.support.Spotlight; import com.exh.math.PtToSeg3; import com.exh.math.RayIntersectsQuad; import com.exh.math.primitive.Geo; import com.exh.math.primitive.Point3; import com.exh.math.primitive.Quad; import com.exh.math.primitive.Segment; public final class Render { // Render fills every pixel and interpolates public final static int RENDER = (1<<0); public final static int SHADOWS = (1<<1); // Ehh, ugly, don't use // Fill fills the quads with a solid colour, stroke draws their outlines public final static int FILL = (1<<2); // Ehh, ugly, don't use public final static int STROKE = (1<<3); // Ehh, ugly, don't use // Spotlight highlights specific quads and geometry public final static int SPOTLIGHT = (1<<4); // Ehh, ugly, don't use // This doesn't belong in the render, it tells the bevel algorithm // to ignore clipping. It's just hacked in for my screenshots public final static int NO_CLIP = (1<<5); private final List mSurface = new LinkedList(); private final Normal mNormal = new Normal(); private final SurfaceShader mShader = new SurfaceShader(); private final SurfaceShader.Params mShaderP = new SurfaceShader.Params(); // For shadow casting private final Segment mShadowRay = new Segment(); private final Point3 mShadowPt = new Point3(); private final RayIntersectsSurface mRIS = new RayIntersectsSurface(); private final PtToSeg3 mPtToSeg3 = new PtToSeg3(); public final static int FG_C = 0xff888888; public final static float STROKE_W = 3; private final static double EDGE_MIN = 0.20f; private final static double EDGE_SCALE = 1 / EDGE_MIN; private final static Point3 LIGHT = newLight(200, -100, 40); private final static Point3 newLight(final double x, final double y, final double z) { final Point3 pt = new Point3(x, y, z); // pt.normalize(); return pt; } public Bitmap newImage( final QuadList ql, final int w, final int h, final int flags, final double maxZ, final Spotlight spot, final ProgressReport progress) { /* SETUP MY SURFACES */ QuadEntry qe = ql.head(); while (qe != null) { final Surface s = new Surface(qe.mQ); mNormal.setOn(qe.mQ, s); mSurface.add(s); qe = qe.mNext; } if (mSurface.size() < 1) return null; // Cache the normal info from all adjacent surfaces. fillAdjacentNormals(); /* CREATE THE IMAGE AND DRAW */ final Bitmap ans = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); ans.eraseColor(0x00000000); if ((flags&RENDER) != 0) { onRender(ans, ql, w, h, flags, maxZ, progress); } final Canvas canvas = new Canvas(ans); final Paint p = new Paint(Paint.ANTI_ALIAS_FLAG); if ((flags&FILL) != 0) { p.setStyle(Paint.Style.FILL); p.setStrokeWidth(1); onDraw(canvas, p, true); } if ((flags&STROKE) != 0) { p.setStyle(Paint.Style.STROKE); p.setColor(0xff000000); p.setStrokeWidth(STROKE_W); onDraw(canvas, p, false); } if ((flags&SPOTLIGHT) != 0) { spot.onDraw(canvas); } return ans; } private void onDraw(final Canvas canvas, final Paint p, final boolean colorWithNormal) { final Path path = new Path(); for (Surface e : mSurface) { path.rewind(); Point3 pt = e.mQ.mPt[e.mQ.mPt.length-1]; path.moveTo((float)pt.x, (float)pt.y); for (int k=0; k 0) { pt.x = (double)x; pt.y = (double)y; if (s == null || !s.mQ.containsCCW(pt.x, pt.y)) { if ((s = getAt(pt.x, pt.y)) != null) { mShader.setTo(s, pt.y); } } if (s == null) { p[pix] = transparent; } else { mShader.getAt(pt.x, mShaderP); p[pix] = makeColor(fgC, srcA, lightFrom(mShaderP.mN, LIGHT) * (mShaderP.mZ / maxZ)); // Fade the edges to black, for a little framing. /* final double pos = mShaderP.mZ/maxZ; if (pos < EDGE_MIN) { p[pix] = scaleColor(p[pix], Geo.sCurve(pos*EDGE_SCALE)); } */ if ((flags&SHADOWS) != 0) { pt.z = mShaderP.mZ + Geo.EPS; double shadow = getShadow(ql, pt, LIGHT); if (shadow > Geo.EPS) { if (shadow > 1) shadow = 1; p[pix] = scaleColor(p[pix], shadow); } } } } } } bm.setPixels(p, 0, w, 0, 0, w, h); } private final int scaleColor(final int c, final double scale) { return Color.argb( Color.alpha(c), (int)(Color.red(c)*scale), (int)(Color.green(c)*scale), (int)(Color.blue(c)*scale)); } // Create a mask the easy way, by rendering the polygon to a bitmap. private final int[] getMask(final QuadList ql, final int w, final int h, final int fgC) { final Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); final Canvas canvas = new Canvas(bm); final Path path = new Path(); final Paint p = new Paint(Paint.ANTI_ALIAS_FLAG); final int[] buf = new int[w*h]; Point3 pt = ql.tail().mQ.mPt[3]; path.moveTo((float)pt.x, (float)pt.y); for (QuadEntry qe : ql) { pt = qe.mQ.mPt[0]; path.lineTo((float)pt.x, (float)pt.y); pt = qe.mQ.mPt[3]; path.lineTo((float)pt.x, (float)pt.y); } bm.eraseColor(Color.argb(0, Color.red(fgC), Color.green(fgC), Color.blue(fgC))); p.setColor(fgC); canvas.drawPath(path, p); bm.getPixels(buf, 0, w, 0, 0, w, h); bm.recycle(); return buf; } private Surface getAt(final double x, final double y) { final int size = mSurface.size(); for (int k=0; k 1) src.mPtNormal[k].div((double)(c[k])); } } } private final double lightFrom(final Point3 normal, final Point3 light) { return (normal.x*light.x + normal.y*light.y + normal.z*light.z) / (normal.length() * light.length()); } private final static int makeColor(final int srcC, final int srcA, final double scale) { int r = Color.red(srcC), g = Color.green(srcC), b = Color.blue(srcC); r = (int)(r + (255*scale)); if (r < 0) r = 0; else if (r > 255) r = 255; g = (int)(g + (255*scale)); if (g < 0) g = 0; else if (g > 255) g = 255; b = (int)(b + (255*scale)); if (b < 0) b = 0; else if (b > 255) b = 255; return Color.argb(srcA, r, g, b); } /* NORMAL * Do the work of generating each surface normal * The normal is the cross product of the vectors * of two non-parallel sides of the surface. ***********************************************/ private final static class Normal { private final Point3 mA = new Point3(), mB = new Point3(); // The surface normal of a quad is the cross product of its diagonals. // Realistically I could just take the normal of one triangle, which // is a little more efficient. void setOn(final Quad q, final Surface s) { mA.asVector(q.mPt[0], q.mPt[2]); mB.asVector(q.mPt[3], q.mPt[1]); s.mNormal.asCrossProduct(mA, mB); s.mNormal.normalize(); } } }