Improved vegetation rendering

- Added early check on terrain height range during iteration
- Added test render of vegetation model
- Tweaked basic tree model
This commit is contained in:
Michaël Lemaire 2015-11-02 20:14:35 +01:00
parent cd006e1093
commit 68945111d1
16 changed files with 338 additions and 37 deletions

View file

@ -2,10 +2,6 @@
#include "PackStream.h" #include "PackStream.h"
Disk::Disk()
{
}
Disk::Disk(const Vector3 &point, const Vector3 &normal, double radius): Disk::Disk(const Vector3 &point, const Vector3 &normal, double radius):
InfinitePlane(point, normal), radius(radius) InfinitePlane(point, normal), radius(radius)
{ {

View file

@ -14,7 +14,7 @@ namespace basics {
class BASICSSHARED_EXPORT Disk: public InfinitePlane class BASICSSHARED_EXPORT Disk: public InfinitePlane
{ {
public: public:
Disk(); Disk() = default;
Disk(const Vector3 &point, const Vector3 &normal, double radius); Disk(const Vector3 &point, const Vector3 &normal, double radius);
inline double getRadius() const {return radius;} inline double getRadius() const {return radius;}

View file

@ -2,6 +2,7 @@
#include <climits> #include <climits>
#include "SpaceGridIterator.h" #include "SpaceGridIterator.h"
using namespace std;
SpaceSegment::SpaceSegment(const Vector3& start, const Vector3& end): SpaceSegment::SpaceSegment(const Vector3& start, const Vector3& end):
start(start), end(end) start(start), end(end)
@ -58,6 +59,44 @@ bool SpaceSegment::intersectYInterval(double ymin, double ymax)
return true; return true;
} }
bool SpaceSegment::intersectBoundingBox(const SpaceSegment &bbox) const
{
Vector3 dir = getDirection();
// r.dir is unit direction vector of ray
double dfx = 1.0 / dir.x;
double dfy = 1.0 / dir.y;
double dfz = 1.0 / dir.z;
// lb is the corner of AABB with minimal coordinates - left bottom, rt is maximal corner
// r.org is origin of ray
double t1 = (bbox.start.x - start.x) * dfx;
double t2 = (bbox.end.x - start.x) * dfx;
double t3 = (bbox.start.y - start.y) * dfy;
double t4 = (bbox.end.y - start.y) * dfy;
double t5 = (bbox.start.z - start.z) * dfz;
double t6 = (bbox.end.z - start.z) * dfz;
double tmin = max(max(min(t1, t2), min(t3, t4)), min(t5, t6));
double tmax = min(min(max(t1, t2), max(t3, t4)), max(t5, t6));
// if tmax < 0, ray (line) is intersecting AABB, but whole AABB is behing us
double t;
if (tmax < 0.0)
{
t = tmax;
return false;
}
// if tmin > tmax, ray doesn't intersect AABB
if (tmin > tmax)
{
t = tmax;
return false;
}
t = tmin;
return true;
}
SpaceSegment SpaceSegment::projectedOnXPlane(double x) const SpaceSegment SpaceSegment::projectedOnXPlane(double x) const
{ {
return SpaceSegment(Vector3(x, start.y, start.z), Vector3(x, end.y, end.z)); return SpaceSegment(Vector3(x, start.y, start.z), Vector3(x, end.y, end.z));

View file

@ -33,6 +33,11 @@ public:
*/ */
bool intersectYInterval(double ymin, double ymax); bool intersectYInterval(double ymin, double ymax);
/**
* Return true if the segment intersects a bounding box, represented by another segment (crossing diagonal).
*/
bool intersectBoundingBox(const SpaceSegment &bbox) const;
/** /**
* Return a version of this segment, projected on a X plane. * Return a version of this segment, projected on a X plane.
*/ */

View file

@ -119,7 +119,6 @@ static void addBranchRecurse(std::vector<CappedCylinder> &branches, const Vector
if (length > 0.1) if (length > 0.1)
{ {
int split_count = 3; int split_count = 3;
Vector3 new_base = base.add(direction.scale(length));
Matrix4 pivot1 = Matrix4::newRotateAxis(randomizeValue(1.0 - 0.6 * length, 0.9, 1.1), VECTOR_EAST); Matrix4 pivot1 = Matrix4::newRotateAxis(randomizeValue(1.0 - 0.6 * length, 0.9, 1.1), VECTOR_EAST);
Vector3 new_direction = pivot1.multPoint(direction); Vector3 new_direction = pivot1.multPoint(direction);
for (int i = 0; i < split_count; i++) for (int i = 0; i < split_count; i++)
@ -127,7 +126,8 @@ static void addBranchRecurse(std::vector<CappedCylinder> &branches, const Vector
Matrix4 pivot2 = Matrix4::newRotateAxis(randomizeValue(M_PI * 2.0 / (double)split_count, 0.9, 1.1), direction); Matrix4 pivot2 = Matrix4::newRotateAxis(randomizeValue(M_PI * 2.0 / (double)split_count, 0.9, 1.1), direction);
new_direction = pivot2.multPoint(new_direction); new_direction = pivot2.multPoint(new_direction);
addBranchRecurse(branches, new_base, new_direction, randomizeValue(radius, 0.65, 0.75), randomizeValue(length, 0.55, 0.65)); Vector3 new_base = base.add(direction.scale(randomizeValue(length, 0.4, 1.0)));
addBranchRecurse(branches, new_base, new_direction, randomizeValue(radius, 0.45, 0.6), randomizeValue(length, 0.55, 0.85));
} }
} }
} }
@ -140,7 +140,7 @@ void VegetationModelDefinition::randomize()
foliage_items.clear(); foliage_items.clear();
// Add trunk and branches // Add trunk and branches
addBranchRecurse(solid_volumes, VECTOR_ZERO, VECTOR_UP, 0.05, 0.5); addBranchRecurse(solid_volumes, VECTOR_ZERO, VECTOR_UP, 0.04, 0.5);
// Add foliage groups // Add foliage groups
for (const auto &branch: solid_volumes) for (const auto &branch: solid_volumes)
@ -150,7 +150,7 @@ void VegetationModelDefinition::randomize()
{ {
double radius = length * 0.5; double radius = length * 0.5;
Vector3 center = branch.getAxis().getOrigin().add(branch.getAxis().getDirection().scale(radius)); Vector3 center = branch.getAxis().getOrigin().add(branch.getAxis().getDirection().scale(radius));
foliage_groups.push_back(Sphere(center, radius * 5.0)); foliage_groups.push_back(Sphere(center, radius * 3.0));
} }
} }
@ -159,7 +159,7 @@ void VegetationModelDefinition::randomize()
{ {
double radius = 0.15; double radius = 0.15;
Vector3 dir = Vector3::randomInSphere(1.0 - radius); Vector3 dir = Vector3::randomInSphere(1.0 - radius);
Vector3 normal = dir.add(Vector3::randomInSphere(0.4)).add(Vector3(0.0, 0.6, 0.0)).normalize(); Vector3 normal = dir.add(Vector3::randomInSphere(0.4)).add(Vector3(0.0, 0.3, 0.0)).normalize();
Disk leaf(dir, normal, randomizeValue(radius, 0.8, 1.0)); Disk leaf(dir, normal, randomizeValue(radius, 0.8, 1.0));
foliage_items.push_back(leaf); foliage_items.push_back(leaf);
} }

View file

@ -17,6 +17,12 @@
#include "LightFilter.h" #include "LightFilter.h"
#include "GodRaysSampler.h" #include "GodRaysSampler.h"
#include "Rasterizer.h" #include "Rasterizer.h"
#include "SpaceSegment.h"
#include "OverlayRasterizer.h"
#include "VegetationModelDefinition.h"
#include "VegetationInstance.h"
#include "VegetationRenderer.h"
#include "RayCastingResult.h"
#include <sstream> #include <sstream>
@ -251,6 +257,50 @@ static void testSunNearHorizon()
} }
} }
static void testVegetationModels()
{
class TestRasterizer: public OverlayRasterizer
{
public:
TestRasterizer(SoftwareCanvasRenderer *renderer, const VegetationModelDefinition &model):
OverlayRasterizer(renderer, renderer->getProgressHelper()),
instance(model, VECTOR_ZERO),
vegetation(renderer->getVegetationRenderer())
{
}
virtual Color processPixel(int, int, double relx, double rely) const override
{
relx *= 0.75;
rely *= 0.75;
SpaceSegment segment(Vector3(relx, rely + 0.5, -5.0), Vector3(relx, rely + 0.5, 5.0));
RayCastingResult result = vegetation->renderInstance(segment, instance, false, true);
return result.hit ? result.hit_color : Color(0.6, 0.7, 0.9);
}
VegetationInstance instance;
VegetationRenderer *vegetation;
};
Scenery scenery;
scenery.autoPreset(1);
scenery.getClouds()->clear();
scenery.getTerrain()->propWaterHeight()->setValue(1.0);
scenery.getCamera()->setTarget(VECTOR_ZERO);
scenery.getCamera()->setLocation(Vector3(0.0, 0.0, -5.0));
int width = 800;
int height = 800;
SoftwareCanvasRenderer renderer(&scenery);
renderer.setSize(width, height);
renderer.setQuality(0.5);
VegetationModelDefinition model(NULL);
renderer.setSoloRasterizer(new TestRasterizer(&renderer, model));
startTestRender(&renderer, "vegetation_model_basic");
}
void runTestSuite() void runTestSuite()
{ {
testGroundShadowQuality(); testGroundShadowQuality();
@ -260,4 +310,5 @@ void runTestSuite()
testNearFrustum(); testNearFrustum();
testCloudsNearGround(); testCloudsNearGround();
testSunNearHorizon(); testSunNearHorizon();
testVegetationModels();
} }

View file

@ -0,0 +1,52 @@
#include "OverlayRasterizer.h"
#include "Color.h"
#include "SoftwareRenderer.h"
#include "CameraDefinition.h"
#include "CanvasFragment.h"
OverlayRasterizer::OverlayRasterizer(SoftwareRenderer *renderer, RenderProgress *progress):
Rasterizer(renderer, progress, 0, COLOR_WHITE)
{
}
int OverlayRasterizer::prepareRasterization()
{
return 1;
}
void OverlayRasterizer::rasterizeToCanvas(CanvasPortion *canvas)
{
double width = (double)renderer->render_camera->getWidth();
double height = (double)renderer->render_camera->getHeight();
Vector3 topleft = renderer->unprojectPoint(Vector3(height, 0.0, 1.0));
Vector3 bottomleft = renderer->unprojectPoint(Vector3(0.0, 0.0, 1.0));
Vector3 topright = renderer->unprojectPoint(Vector3(height, width, 1.0));
Vector3 bottomright = renderer->unprojectPoint(Vector3(0.0, width, 1.0));
pushQuad(canvas, topleft, bottomleft, bottomright, topright);
}
Color OverlayRasterizer::shadeFragment(const CanvasFragment &fragment, const CanvasFragment *) const
{
double width = (double)renderer->render_camera->getWidth() - 1.0;
double height = (double)renderer->render_camera->getHeight() - 1.0;
double relx;
double rely;
double x = floor(fragment.getPixel().x);
double y = floor(fragment.getPixel().y);
if (width > height)
{
relx = 2.0 * ((x - (width - height) * 0.5) / height - 0.5);
rely = 2.0 * (y / height - 0.5);
}
else
{
relx = 2.0 * (x / height - 0.5);
rely = 2.0 * ((y - (height - width) * 0.5) / height - 0.5);
}
return processPixel((int)x, (int)y, relx, rely);
}

View file

@ -0,0 +1,35 @@
#ifndef OVERLAYRASTERIZER_H
#define OVERLAYRASTERIZER_H
#include "software_global.h"
#include "Rasterizer.h"
namespace paysages {
namespace software {
/**
* Base class for overlay rasterizer.
*
* It's a rasterizer that puts a single quad in front of camera, in order to apply a shader on each pixel.
*/
class SOFTWARESHARED_EXPORT OverlayRasterizer: public Rasterizer
{
public:
OverlayRasterizer(SoftwareRenderer *renderer, RenderProgress *progress);
/**
* Abstract method to implement to shade each pixel.
*/
virtual Color processPixel(int x, int y, double relx, double rely) const = 0;
private:
virtual int prepareRasterization();
virtual void rasterizeToCanvas(CanvasPortion *canvas);
virtual Color shadeFragment(const CanvasFragment &fragment, const CanvasFragment *previous) const;
};
}
}
#endif // OVERLAYRASTERIZER_H

View file

@ -220,3 +220,18 @@ bool TerrainRenderer::applyLightFilter(LightComponent &light, const Vector3 &at)
return true; return true;
} }
} }
void TerrainRenderer::estimateMinMaxHeight(double x1, double z1, double x2, double z2, double *ymin, double *ymax)
{
double y1 = getHeight(x1, z1, true);
double y2 = getHeight(x2, z2, true);
// TODO Add quality factor
// TODO Use all 4 corners
// TODO Apply max slope
// TODO Estimate displacement
std::pair<double, double>minmax = std::minmax(y1, y2);
*ymin = minmax.first;
*ymax = minmax.second;
}

View file

@ -34,6 +34,11 @@ public:
virtual Color getFinalColor(const Vector3 &location, double precision); virtual Color getFinalColor(const Vector3 &location, double precision);
virtual bool applyLightFilter(LightComponent &light, const Vector3 &at) override; virtual bool applyLightFilter(LightComponent &light, const Vector3 &at) override;
/**
* Estimate a probable range of altitudes, given a rectangle area.
*/
void estimateMinMaxHeight(double x1, double z1, double x2, double z2, double *ymin, double *ymax);
private: private:
SoftwareRenderer *parent; SoftwareRenderer *parent;
TerrainRayWalker *walker_ray; TerrainRayWalker *walker_ray;

View file

@ -113,7 +113,6 @@ VegetationResult VegetationModelRenderer::getResult(const SpaceSegment &segment,
material.shininess = 3.0; material.shininess = 3.0;
material.hardness = 0.3; material.hardness = 0.3;
material.validate(); material.validate();
// FIXME Can't use reference to temporary material
return VegetationResult(location, normal, material); return VegetationResult(location, normal, material);
} }
else else

View file

@ -21,8 +21,8 @@
class VegetationGridIterator: public SpaceGridIterator class VegetationGridIterator: public SpaceGridIterator
{ {
public: public:
VegetationGridIterator(const SpaceSegment &segment, VegetationRenderer *renderer, VegetationLayerDefinition *layer, bool only_hit): VegetationGridIterator(const SpaceSegment &segment, VegetationRenderer *renderer, bool only_hit):
segment(segment), renderer(renderer), layer(layer), only_hit(only_hit) segment(segment), renderer(renderer), only_hit(only_hit)
{ {
} }
@ -30,24 +30,12 @@ public:
virtual bool onCell(int x, int, int z) override virtual bool onCell(int x, int, int z) override
{ {
std::vector<VegetationInstance> instances; result = renderer->getBoundResult(segment, (double)x, (double)z, only_hit);
return not result.hit;
layer->getPresence()->collectInstances(&instances, *layer->getModel(), x - 0.5, z - 0.5, x + 0.5, z + 0.5);
for (auto &instance: instances)
{
result = renderer->renderInstance(segment, instance, only_hit);
if (result.hit)
{
return false;
}
}
return true;
} }
private: private:
const SpaceSegment &segment; const SpaceSegment &segment;
VegetationRenderer *renderer; VegetationRenderer *renderer;
VegetationLayerDefinition *layer;
RayCastingResult result; RayCastingResult result;
bool only_hit; bool only_hit;
}; };
@ -97,17 +85,11 @@ RayCastingResult VegetationRenderer::getResult(const SpaceSegment &segment, bool
{ {
if (enabled) if (enabled)
{ {
VegetationDefinition *vegetation = parent->getScenery()->getVegetation(); // Find instances potentially crossing the segment
int n = vegetation->count(); VegetationGridIterator it(segment, this, only_hit);
// TODO Don't stop at first layer, find the nearest hit if (not segment.projectedOnYPlane().iterateOnGrid(it))
for (int i = 0; i < n; i++)
{ {
// Find instances potentially crossing the segment return it.getResult();
VegetationGridIterator it(segment, this, vegetation->getVegetationLayer(i), only_hit);
if (not segment.projectedOnYPlane().iterateOnGrid(it))
{
return it.getResult();
}
} }
return RayCastingResult(); return RayCastingResult();
} }
@ -117,6 +99,42 @@ RayCastingResult VegetationRenderer::getResult(const SpaceSegment &segment, bool
} }
} }
RayCastingResult VegetationRenderer::getBoundResult(const SpaceSegment &segment, double x, double z, bool only_hit, double xsize, double zsize)
{
// Early check if we may cross any vegetation
double ymin, ymax;
double vegetation_max_height = 0.0; // TODO
parent->getTerrainRenderer()->estimateMinMaxHeight(x, z, x + xsize, z + zsize, &ymin, &ymax);
ymax += vegetation_max_height;
SpaceSegment bbox(Vector3(x, ymin, z), Vector3(x + xsize, ymax, z + zsize));
if (not segment.intersectBoundingBox(bbox)) {
return RayCastingResult();
}
// Iterate all layers and instances
VegetationDefinition *vegetation = parent->getScenery()->getVegetation();
int n = vegetation->count();
for (int i = 0; i < n; i++)
{
VegetationLayerDefinition *layer = vegetation->getVegetationLayer(i);
std::vector<VegetationInstance> instances;
layer->getPresence()->collectInstances(&instances, *layer->getModel(), x, z, x + xsize, z + zsize);
for (auto &instance: instances)
{
RayCastingResult result = renderInstance(segment, instance, only_hit);
if (result.hit)
{
// TODO Don't stop at first hit, find the nearest one
return result;
}
}
}
return RayCastingResult();
}
bool VegetationRenderer::applyLightFilter(LightComponent &light, const Vector3 &at) bool VegetationRenderer::applyLightFilter(LightComponent &light, const Vector3 &at)
{ {
if (enabled) if (enabled)

View file

@ -34,6 +34,11 @@ public:
*/ */
RayCastingResult getResult(const SpaceSegment &segment, bool only_hit=false); RayCastingResult getResult(const SpaceSegment &segment, bool only_hit=false);
/**
* Perform ray casting on a squared region.
*/
RayCastingResult getBoundResult(const SpaceSegment &segment, double x, double z, bool only_hit=false, double xsize=1.0, double zsize=1.0);
virtual bool applyLightFilter(LightComponent &light, const Vector3 &at) override; virtual bool applyLightFilter(LightComponent &light, const Vector3 &at) override;
private: private:

View file

@ -36,6 +36,7 @@ namespace software {
class WaterRenderer; class WaterRenderer;
class Rasterizer; class Rasterizer;
class OverlayRasterizer;
class SkyRasterizer; class SkyRasterizer;
class TerrainRasterizer; class TerrainRasterizer;

View file

@ -0,0 +1,65 @@
#include "BaseTestCase.h"
#include "OverlayRasterizer.h"
#include <vector>
#include "Scenery.h"
#include "SoftwareCanvasRenderer.h"
#include "Color.h"
typedef struct {
int x;
int y;
double relx;
double rely;
} PixelCall;
std::vector<PixelCall> calls;
class MockOverlayRasterizer: public OverlayRasterizer
{
public:
MockOverlayRasterizer(SoftwareCanvasRenderer *renderer):
OverlayRasterizer(renderer, renderer->getProgressHelper())
{
}
virtual Color processPixel(int x, int y, double relx, double rely) const override
{
PixelCall call = {x, y, relx, rely};
calls.push_back(call);
return COLOR_BLUE;
}
};
void checkCall(const PixelCall &call, int x, int y, double relx, double rely)
{
EXPECT_EQ(x, call.x);
EXPECT_EQ(y, call.y);
EXPECT_DOUBLE_EQ(relx, call.relx);
EXPECT_DOUBLE_EQ(rely, call.rely);
}
TEST(OverlayRasterizer, pixelProcessing)
{
calls.clear();
Scenery scenery;
SoftwareCanvasRenderer renderer(&scenery);
renderer.setSize(4, 3);
renderer.setSoloRasterizer(new MockOverlayRasterizer(&renderer));
renderer.render();
ASSERT_EQ(12, (int)calls.size());
checkCall(calls[0], 0, 0, -1.5, -1.0);
checkCall(calls[1], 0, 2, -1.5, 1.0);
checkCall(calls[2], 2, 0, 0.5, -1.0);
checkCall(calls[3], 2, 2, 0.5, 1.0);
checkCall(calls[4], 0, 1, -1.5, 0.0);
checkCall(calls[5], 1, 0, -0.5, -1.0);
checkCall(calls[6], 1, 1, -0.5, 0.0);
checkCall(calls[7], 1, 2, -0.5, 1.0);
checkCall(calls[8], 2, 1, 0.5, 0.0);
checkCall(calls[9], 3, 0, 1.5, -1.0);
checkCall(calls[10], 3, 1, 1.5, 0.0);
checkCall(calls[11], 3, 2, 1.5, 1.0);
}

View file

@ -59,3 +59,18 @@ TEST(SpaceSegment, iterateOnGrid_Negative)
ASSERT_EQ(1, (int)it.locations.size()); ASSERT_EQ(1, (int)it.locations.size());
EXPECT_VECTOR3_COORDS(it.locations[0], -9.0, -9.0, -9.0); EXPECT_VECTOR3_COORDS(it.locations[0], -9.0, -9.0, -9.0);
} }
TEST(SpaceSegment, intersectBoundingBox)
{
SpaceSegment bbox(Vector3(-1.0, -1.0, -1.0), Vector3(1.0, 4.0, 1.0));
EXPECT_TRUE(SpaceSegment(Vector3(0.0, 0.0, 0.0), Vector3(1.0, 1.0, 1.0)).intersectBoundingBox(bbox));
EXPECT_TRUE(SpaceSegment(Vector3(-5.0, 0.0, 0.0), Vector3(5.0, 0.0, 0.0)).intersectBoundingBox(bbox));
EXPECT_FALSE(SpaceSegment(Vector3(-5.0, -2.0, 0.0), Vector3(5.0, -2.0, 0.0)).intersectBoundingBox(bbox));
EXPECT_TRUE(SpaceSegment(Vector3(-5.0, 0.0, -5.0), Vector3(5.0, 0.0, 5.0)).intersectBoundingBox(bbox));
EXPECT_FALSE(SpaceSegment(Vector3(-5.0, -2.0, 0.0), Vector3(5.0, -2.0, 5.0)).intersectBoundingBox(bbox));
EXPECT_FALSE(SpaceSegment(Vector3(-10.0, 0.0, 0.0), Vector3(10.0, -6.0, 0.0)).intersectBoundingBox(bbox));
}