-
-
Save jcfandino/cf9c23eaf360f70fb307388c68fd845e to your computer and use it in GitHub Desktop.
Decal Projector
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.stovokor.decal; | |
import com.jme3.app.SimpleApplication; | |
import com.jme3.asset.plugins.ClasspathLocator; | |
import com.jme3.input.KeyInput; | |
import com.jme3.input.controls.ActionListener; | |
import com.jme3.input.controls.KeyTrigger; | |
import com.jme3.light.AmbientLight; | |
import com.jme3.material.Material; | |
import com.jme3.math.ColorRGBA; | |
import com.jme3.math.Vector3f; | |
import com.jme3.renderer.queue.RenderQueue.Bucket; | |
import com.jme3.scene.Geometry; | |
import com.jme3.scene.Spatial; | |
import com.jme3.scene.shape.Box; | |
public class DecalDemo extends SimpleApplication implements ActionListener { | |
public static void main(String[] args) { | |
new DecalDemo().start(); | |
} | |
private Spatial map; | |
@Override | |
public void simpleInitApp() { | |
cam.setLocation(new Vector3f(0, 1, 4)); | |
flyCam.setRotationSpeed(2f); | |
flyCam.setMoveSpeed(20f); | |
assetManager.registerLocator("levels", ClasspathLocator.class); | |
map = assetManager.loadModel("levels/cryo.j3o"); | |
map.setQueueBucket(Bucket.Transparent); | |
rootNode.attachChild(map); | |
rootNode.addLight(new AmbientLight(ColorRGBA.White)); | |
inputManager.addMapping("splat", new KeyTrigger(KeyInput.KEY_SPACE)); | |
inputManager.addListener(this, "splat"); | |
} | |
@Override | |
public void onAction(String name, boolean isPressed, float tpf) { | |
if (name.equals("splat") && isPressed) { | |
var dist = 10f; | |
var pos = cam.getLocation().add(cam.getDirection().mult(dist / 2)); | |
var rot = cam.getRotation(); | |
var projectionBox = new Vector3f(1f, 1f, dist); | |
var projector = new DecalProjector(map, pos, rot, projectionBox); | |
var geometry = projector.project(); | |
geometry.setMaterial(assetManager.loadMaterial("Materials/surface/Test.j3m")); | |
rootNode.attachChild(geometry); | |
var box = new Geometry("box", new Box(projectionBox.x / 2f, projectionBox.y / 2f, projectionBox.z / 2f)); | |
var boxmat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); | |
boxmat.getAdditionalRenderState().setWireframe(true); | |
boxmat.setColor("Color", ColorRGBA.White); | |
box.setMaterial(boxmat); | |
box.setLocalTranslation(pos); | |
box.setLocalRotation(rot); | |
rootNode.attachChild(box); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.stovokor.decal; | |
import com.jme3.math.FastMath; | |
import com.jme3.math.Matrix4f; | |
import com.jme3.math.Quaternion; | |
import com.jme3.math.Transform; | |
import com.jme3.math.Vector2f; | |
import com.jme3.math.Vector3f; | |
import com.jme3.scene.Geometry; | |
import com.jme3.scene.Mesh; | |
import com.jme3.scene.Spatial; | |
import com.jme3.scene.VertexBuffer; | |
import com.jme3.scene.VertexBuffer.Type; | |
import com.jme3.util.BufferUtils; | |
import java.nio.FloatBuffer; | |
import java.util.ArrayList; | |
import java.util.Collection; | |
import java.util.List; | |
public class DecalProjector { | |
private List<Geometry> geometries; | |
private Vector3f size; | |
private Matrix4f projectorMatrix; | |
private Matrix4f projectorMatrixInverse; | |
private float separation; | |
public DecalProjector(Spatial spatial, Vector3f position, Quaternion rotation, Vector3f size) { | |
var geometries = new ArrayList<Geometry>(); | |
spatial.depthFirstTraversal(s -> { | |
if (s instanceof Geometry) { | |
geometries.add((Geometry) s); | |
} | |
}); | |
setGeometries(geometries); | |
setSize(size); | |
setSeparation(0.0001f); | |
setTransform(new Transform(position, rotation, new Vector3f(1, 1, 1))); | |
} | |
public DecalProjector(Collection<Geometry> geometries, Vector3f position, Quaternion rotation, Vector3f size) { | |
this(geometries, position, rotation, size, 0.0001f); | |
} | |
public DecalProjector(Collection<Geometry> geometries, Vector3f position, Quaternion rotation, Vector3f size, | |
float separation) { | |
setSize(size); | |
setGeometries(geometries); | |
setSeparation(separation); | |
setTransform(new Transform(position, rotation, new Vector3f(1, 1, 1))); | |
} | |
public void setSize(Vector3f size) { | |
this.size = size; | |
} | |
public void setGeometries(Collection<Geometry> geometries) { | |
this.geometries = List.copyOf(geometries); | |
} | |
public void setSeparation(float separation) { | |
this.separation = separation; | |
} | |
public void setTransform(Transform transform) { | |
projectorMatrix = transform.toTransformMatrix(); | |
projectorMatrixInverse = projectorMatrix.invert(); | |
} | |
public Geometry project() { | |
// first, create an array of 'DecalVertex' objects | |
// three consecutive 'DecalVertex' objects represent a single face | |
// this data structure will be later used to perform the clipping | |
var decalVertices = new ArrayList<DecalVertex>(); | |
for (var geometry : geometries) { | |
geometry.computeWorldMatrix(); | |
var mesh = geometry.getMesh(); | |
var positions = getVectors(mesh, VertexBuffer.Type.Position); | |
var normals = getVectors(mesh, VertexBuffer.Type.Normal); | |
var indices = mesh.getIndexBuffer(); | |
for (var i = 0; i < indices.size(); i++) { | |
var index = indices.get(i); | |
pushDecalVertex(geometry, decalVertices, positions[index], normals[index]); | |
} | |
} | |
// second, clip the geometry so that it doesn't extend out from the projector | |
decalVertices = clipVertices(decalVertices); | |
// third, generate final vertices, normals and uvs | |
var decalUvs = new Vector2f[decalVertices.size()]; | |
var decalPositions = new Vector3f[decalVertices.size()]; | |
var decalNormals = new Vector3f[decalVertices.size()]; | |
var decalIndices = new int[3 * decalVertices.size()]; | |
var i = 0; | |
for (var decalVertex : decalVertices) { | |
// create texture coordinates (we are still in projector space) | |
decalUvs[i] = new Vector2f(0.5f + (decalVertex.position.x / size.x), 0.5f + (decalVertex.position.y / size.y)); | |
// transform the vertex back to world space | |
projectorMatrix.mult(decalVertex.position, decalVertex.position); | |
// now create vertex and normal buffer data | |
decalPositions[i] = decalVertex.position; | |
decalNormals[i] = decalVertex.normal; | |
decalIndices[i] = i++; | |
} | |
var decalMesh = new Mesh(); | |
decalMesh.setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(decalPositions)); | |
decalMesh.setBuffer(Type.Index, 1, BufferUtils.createIntBuffer(decalIndices)); | |
decalMesh.setBuffer(Type.Normal, 3, BufferUtils.createFloatBuffer(decalNormals)); | |
decalMesh.setBuffer(Type.TexCoord, 2, BufferUtils.createFloatBuffer(decalUvs)); | |
decalMesh.updateBound(); | |
return new Geometry("decal", decalMesh); | |
} | |
protected ArrayList<DecalVertex> clipVertices(ArrayList<DecalVertex> decalVertices) { | |
decalVertices = clipGeometry(decalVertices, new Vector3f(1, 0, 0)); | |
decalVertices = clipGeometry(decalVertices, new Vector3f(-1, 0, 0)); | |
decalVertices = clipGeometry(decalVertices, new Vector3f(0, 1, 0)); | |
decalVertices = clipGeometry(decalVertices, new Vector3f(0, -1, 0)); | |
decalVertices = clipGeometry(decalVertices, new Vector3f(0, 0, 1)); | |
decalVertices = clipGeometry(decalVertices, new Vector3f(0, 0, -1)); | |
return decalVertices; | |
} | |
private void pushDecalVertex(Geometry geometry, List<DecalVertex> vertices, Vector3f pos, Vector3f n) { | |
var position = pos.clone(); | |
var normal = n.clone(); | |
// move the vertex away from the original (to avoid z-fighting) | |
position.addLocal(normal.mult(separation)); | |
// transform the vertex to world space, then to projector space | |
geometry.getWorldMatrix().mult(position, position); | |
projectorMatrixInverse.mult(position, position); | |
geometry.getWorldMatrix().rotateVect(normal); | |
vertices.add(new DecalVertex(position, normal)); | |
} | |
private ArrayList<DecalVertex> clipGeometry(List<DecalVertex> inVertices, Vector3f plane) { | |
var outVertices = new ArrayList<DecalVertex>(); | |
var s = 0.5f * FastMath.abs(size.dot(plane)); | |
// a single iteration clips one face, | |
// which consists of three consecutive 'DecalVertex' objects | |
for (var i = 0; i < inVertices.size(); i += 3) { | |
var total = 0; | |
DecalVertex nV1 = null; | |
DecalVertex nV2 = null; | |
DecalVertex nV3 = null; | |
DecalVertex nV4 = null; | |
var d1 = inVertices.get(i + 0).position.dot(plane) - s; | |
var d2 = inVertices.get(i + 1).position.dot(plane) - s; | |
var d3 = inVertices.get(i + 2).position.dot(plane) - s; | |
var v1Out = d1 > 0; | |
var v2Out = d2 > 0; | |
var v3Out = d3 > 0; | |
// calculate, how many vertices of the face lie outside of the clipping plane | |
total = (v1Out ? 1 : 0) + (v2Out ? 1 : 0) + (v3Out ? 1 : 0); | |
switch (total) { | |
case 0: { | |
// the entire face lies inside of the plane, no clipping needed | |
outVertices.add(inVertices.get(i)); | |
outVertices.add(inVertices.get(i + 1)); | |
outVertices.add(inVertices.get(i + 2)); | |
break; | |
} | |
case 1: { | |
// one vertex lies outside of the plane, perform clipping | |
if (v1Out) { | |
nV1 = inVertices.get(i + 1); | |
nV2 = inVertices.get(i + 2); | |
nV3 = clip(inVertices.get(i), nV1, plane, s); | |
nV4 = clip(inVertices.get(i), nV2, plane, s); | |
} | |
if (v2Out) { | |
nV1 = inVertices.get(i); | |
nV2 = inVertices.get(i + 2); | |
nV3 = clip(inVertices.get(i + 1), nV1, plane, s); | |
nV4 = clip(inVertices.get(i + 1), nV2, plane, s); | |
outVertices.add(nV3); | |
outVertices.add(nV2.clone()); | |
outVertices.add(nV1.clone()); | |
outVertices.add(nV2.clone()); | |
outVertices.add(nV3.clone()); | |
outVertices.add(nV4); | |
break; | |
} | |
if (v3Out) { | |
nV1 = inVertices.get(i); | |
nV2 = inVertices.get(i + 1); | |
nV3 = clip(inVertices.get(i + 2), nV1, plane, s); | |
nV4 = clip(inVertices.get(i + 2), nV2, plane, s); | |
} | |
outVertices.add(nV1.clone()); | |
outVertices.add(nV2.clone()); | |
outVertices.add(nV3); | |
outVertices.add(nV4); | |
outVertices.add(nV3.clone()); | |
outVertices.add(nV2.clone()); | |
break; | |
} | |
case 2: { | |
// two vertices lies outside of the plane, perform clipping | |
if (!v1Out) { | |
nV1 = inVertices.get(i).clone(); | |
nV2 = clip(nV1, inVertices.get(i + 1), plane, s); | |
nV3 = clip(nV1, inVertices.get(i + 2), plane, s); | |
outVertices.add(nV1); | |
outVertices.add(nV2); | |
outVertices.add(nV3); | |
} | |
if (!v2Out) { | |
nV1 = inVertices.get(i + 1).clone(); | |
nV2 = clip(nV1, inVertices.get(i + 2), plane, s); | |
nV3 = clip(nV1, inVertices.get(i), plane, s); | |
outVertices.add(nV1); | |
outVertices.add(nV2); | |
outVertices.add(nV3); | |
} | |
if (!v3Out) { | |
nV1 = inVertices.get(i + 2).clone(); | |
nV2 = clip(nV1, inVertices.get(i), plane, s); | |
nV3 = clip(nV1, inVertices.get(i + 1), plane, s); | |
outVertices.add(nV1); | |
outVertices.add(nV2); | |
outVertices.add(nV3); | |
} | |
break; | |
} | |
case 3: { | |
// the entire face lies outside of the plane, so let's discard the corresponding | |
// vertices | |
break; | |
} | |
} | |
} | |
return outVertices; | |
} | |
private DecalVertex clip(DecalVertex v0, DecalVertex v1, Vector3f p, float s) { | |
var d0 = v0.position.dot(p) - s; | |
var d1 = v1.position.dot(p) - s; | |
var s0 = d0 / (d0 - d1); | |
var v = new DecalVertex( | |
new Vector3f(v0.position.x + s0 * (v1.position.x - v0.position.x), | |
v0.position.y + s0 * (v1.position.y - v0.position.y), v0.position.z + s0 * (v1.position.z - v0.position.z)), | |
new Vector3f(v0.normal.x + s0 * (v1.normal.x - v0.normal.x), v0.normal.y + s0 * (v1.normal.y - v0.normal.y), | |
v0.normal.z + s0 * (v1.normal.z - v0.normal.z))); | |
// need to clip more values (texture coordinates)? do it this way: | |
// intersectpoint.value = a.value + s * ( b.value - a.value ); | |
return v; | |
} | |
private Vector3f[] getVectors(Mesh mesh, Type bufferType) { | |
var buffer = mesh.getBuffer(bufferType); | |
var data = (FloatBuffer) buffer.getDataReadOnly(); | |
return BufferUtils.getVector3Array(data); | |
} | |
protected class DecalVertex { | |
private final Vector3f position; | |
private final Vector3f normal; | |
private DecalVertex(Vector3f p, Vector3f n) { | |
this.position = p; | |
this.normal = n; | |
} | |
public DecalVertex clone() { | |
return new DecalVertex(position.clone(), normal.clone()); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment