/*
* Copyright (c) 2015 NOVA, All rights reserved.
* This library is free software, licensed under GNU Lesser General Public License version 3
*
* This file is part of NOVA.
*
* NOVA is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NOVA 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 for more details.
*
* You should have received a copy of the GNU General Public License
* along with NOVA. If not, see <http://www.gnu.org/licenses/>.
*/
package nova.core.util;
import nova.core.block.Block;
import nova.core.component.ComponentMap;
import nova.core.component.ComponentProvider;
import nova.core.component.misc.Collider;
import nova.core.component.transform.WorldTransform;
import nova.core.entity.Entity;
import nova.core.entity.component.Living;
import nova.core.util.math.Vector3DUtil;
import nova.core.util.shape.Cuboid;
import nova.core.world.World;
import org.apache.commons.math3.geometry.euclidean.threed.Vector3D;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
/**
* Ray tracing for cuboids.
* @author Calclavia
*/
//TODO: Add ray trace masks
public class RayTracer {
public final Ray ray;
public double distance;
public int parallelThreshold = 1000;
public RayTracer(Ray ray) {
this.ray = ray;
}
/**
* Does an entity look ray trace to see which block the entity is looking at.
* @param entity The entity
*/
public RayTracer(Entity entity) {
this(new Ray(entity.position().add(entity.components.getOp(Living.class).map(l -> l.faceDisplacement.get()).orElse(Vector3D.ZERO)), entity.rotation().applyTo(Vector3DUtil.FORWARD)));
}
/**
* Sets the distance of the ray
* @param distance Distance in meters
* @return This
*/
public RayTracer setDistance(double distance) {
this.distance = distance;
return this;
}
public RayTracer setParallelThreshold(int parallelThreshold) {
this.parallelThreshold = parallelThreshold;
return this;
}
public boolean doParallel() {
return distance > parallelThreshold;
}
public Stream<RayTraceResult> rayTraceAll(World world) {
return Stream.concat(rayTraceBlocks(world), rayTraceEntities(world)).sorted();
}
/**
* Check all blocks that are in a line
* @return The blocks ray traced in the order from closest to furthest.
*/
public Stream<RayTraceBlockResult> rayTraceBlocks(World world) {
//All relevant blocks
return rayTraceBlocks(
IntStream.range(0, (int) distance + 1)
.mapToObj(i -> ray.origin.add(ray.dir.scalarMultiply(i)))
.flatMap(vec -> Arrays.stream(Direction.VALID_DIRECTIONS).map(direction -> Vector3DUtil.floor(vec.add(direction.toVector())))) //Cover a larger area to be safe
.distinct()
.map(world::getBlock)
.filter(Optional::isPresent)
.map(Optional::get)
);
}
/**
* Ray traces a set of blocks
* @param blocks Set of blocks
* @return A list of cuboids that intersect with the line segment in the order from closest to furthest.
*/
public Stream<RayTraceBlockResult> rayTraceBlocks(Set<Block> blocks) {
return rayTraceBlocks(blocks.stream());
}
public Stream<RayTraceBlockResult> rayTraceBlocks(Stream<Block> blockStream) {
return
(doParallel() ? blockStream.parallel() : blockStream)
.filter(block -> block.components.has(Collider.class))
.flatMap(block -> rayTraceCollider(block, (pos, cuboid) -> new RayTraceBlockResult(pos, ray.origin.distance(pos), cuboid.sideOf(pos), cuboid, block)))
.sorted();
}
public Stream<RayTraceEntityResult> rayTraceEntities(World world) {
//TODO: Consider smaller check space
return rayTraceEntities(
world.getEntities(Cuboid.ZERO.expand(distance).add(ray.origin))
.stream()
.filter(entity -> entity.components.has(Collider.class))
);
}
public Stream<RayTraceEntityResult> rayTraceEntities(Stream<Entity> entityStream) {
return
(doParallel() ? entityStream.parallel() : entityStream)
.filter(entity -> entity.components.has(Collider.class))
.flatMap(entity -> rayTraceCollider(entity, (pos, cuboid) -> new RayTraceEntityResult(pos, ray.origin.distance(pos), cuboid.sideOf(pos), cuboid, entity)))
.sorted();
}
@SuppressWarnings("rawtypes")
public <R extends RayTraceResult> Stream<R> rayTraceCollider(ComponentProvider<? extends ComponentMap> colliderProvider, BiFunction<Vector3D, Cuboid, R> resultMapper) {
return
colliderProvider.components.get(Collider.class)
.occlusionBoxes
.apply(Optional.empty())
.stream()
.map(cuboid -> cuboid.add((Vector3D) colliderProvider.components.get(WorldTransform.class).position()))
.map(cuboid -> rayTrace(cuboid, resultMapper))
.filter(Optional::isPresent)
.map(Optional::get);
}
/**
* Ray traces a set of cuboids
* @param stream A stream of cuboids
* @return A list of cuboids that intersect with the line segment in the order from closest to furthest.
*/
public List<RayTraceResult> rayTrace(Stream<Cuboid> stream) {
return stream
.map(this::rayTrace)
.filter(Optional::isPresent)
.map(Optional::get)
.sorted()
.collect(Collectors.toList());
}
public Optional<RayTraceResult> rayTrace(Cuboid cuboid) {
return rayTrace(cuboid, (pos, cuboid2) -> new RayTraceResult(pos, ray.origin.distance(pos), cuboid.sideOf(pos), cuboid));
}
/**
* Ray traces a cuboid
* @param cuboid The cuboid in absolute world coordinates
* @return The ray trace result if the ray intersects the cuboid
*/
public <R extends RayTraceResult> Optional<R> rayTrace(Cuboid cuboid, BiFunction<Vector3D, Cuboid, R> resultMapper) {
return rayTrace(cuboid, 0, distance).map(vec -> resultMapper.apply(vec, cuboid));
}
/**
* Calculates intersection with the given ray between a certain distance
* interval.
* <p>
* Ray-box intersection is using IEEE numerical properties to ensure the
* test is both robust and efficient, as described in:
* <br>
* <code>Amy Williams, Steve Barrus, R. Keith Morley, and Peter Shirley: "An
* Efficient and Robust Ray-Box Intersection Algorithm" Journal of graphics
* tools, 10(1):49-54, 2005</code>
* @param cuboid The cuboid to trace
* @param minDist The minimum distance
* @param maxDist The maximum distance
* @return intersection point on the bounding box (only the first is
* returned) or null if no intersection
*/
public Optional<Vector3D> rayTrace(Cuboid cuboid, double minDist, double maxDist) {
Vector3D bbox;
double tMin;
double tMax;
bbox = ray.signDirX ? cuboid.max : cuboid.min;
tMin = (bbox.getX() - ray.origin.getX()) * ray.invDir.getX();
bbox = ray.signDirX ? cuboid.min : cuboid.max;
tMax = (bbox.getX() - ray.origin.getX()) * ray.invDir.getX();
//Y
bbox = ray.signDirY ? cuboid.max : cuboid.min;
double tyMin = (bbox.getY() - ray.origin.getY()) * ray.invDir.getY();
bbox = ray.signDirY ? cuboid.min : cuboid.max;
double tyMax = (bbox.getY() - ray.origin.getY()) * ray.invDir.getY();
//Check with the current tMin and tMax to see if the clipping is out of bounds
if ((tMin > tyMax) || (tyMin > tMax)) {
return Optional.empty();
}
//Reset tMin and tMax
if (tyMin > tMin) {
tMin = tyMin;
}
if (tyMax < tMax) {
tMax = tyMax;
}
bbox = ray.signDirZ ? cuboid.max : cuboid.min;
double tzMin = (bbox.getZ() - ray.origin.getZ()) * ray.invDir.getZ();
bbox = ray.signDirZ ? cuboid.min : cuboid.max;
double tzMax = (bbox.getZ() - ray.origin.getZ()) * ray.invDir.getZ();
//Check with the current tMin and tMax to see if the clipping is out of bounds
if ((tMin > tzMax) || (tzMin > tMax)) {
return Optional.empty();
}
//Reset tMin and tMax
if (tzMin > tMin) {
tMin = tzMin;
}
if (tzMax < tMax) {
tMax = tzMax;
}
if ((tMin < maxDist) && (tMax > minDist)) {
return Optional.of(ray.origin.add(ray.dir.scalarMultiply(tMin)));
}
return Optional.empty();
}
public static class RayTraceResult implements Comparable<RayTraceResult> {
public final Vector3D hit;
public final double distance;
public final Direction side;
public final Cuboid hitCuboid;
public RayTraceResult(Vector3D hit, double distance, Direction side, Cuboid hitCuboid) {
this.hit = hit;
this.distance = distance;
this.side = side;
this.hitCuboid = hitCuboid;
}
@Override
public int compareTo(RayTraceResult o) {
return distance == o.distance ? 0 : distance < o.distance ? -1 : 1;
}
}
public static class RayTraceBlockResult extends RayTraceResult {
public final Block block;
public RayTraceBlockResult(Vector3D hit, double distance, Direction side, Cuboid hitCuboid, Block block) {
super(hit, distance, side, hitCuboid);
this.block = block;
}
}
public static class RayTraceEntityResult extends RayTraceResult {
public final Entity entity;
public RayTraceEntityResult(Vector3D hit, double distance, Direction side, Cuboid hitCuboid, Entity entity) {
super(hit, distance, side, hitCuboid);
this.entity = entity;
}
}
}