542 lines
19 KiB
C++
542 lines
19 KiB
C++
/* -*-c++-*- OpenSceneGraph - Copyright (C) 1998-2006 Robert Osfield
|
|
*
|
|
* This library is open source and may be redistributed and/or modified under
|
|
* the terms of the OpenSceneGraph Public License (OSGPL) version 0.0 or
|
|
* (at your option) any later version. The full license is in LICENSE file
|
|
* included with this distribution, and on the openscenegraph.org website.
|
|
*
|
|
* This library 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
|
|
* OpenSceneGraph Public License for more details.
|
|
*
|
|
* ViewDependentShadow codes Copyright (C) 2008 Wojciech Lewandowski
|
|
* Thanks to to my company http://www.ai.com.pl for allowing me free this work.
|
|
*/
|
|
|
|
#include <osgShadow/MinimalShadowMap>
|
|
#include <osgShadow/ConvexPolyhedron>
|
|
#include <osg/MatrixTransform>
|
|
#include <osgShadow/ShadowedScene>
|
|
#include <osg/ComputeBoundsVisitor>
|
|
|
|
using namespace osgShadow;
|
|
|
|
#define PRINT_SHADOW_TEXEL_TO_PIXEL_ERROR 0
|
|
|
|
MinimalShadowMap::MinimalShadowMap():
|
|
BaseClass(),
|
|
_maxFarPlane( FLT_MAX ),
|
|
_minLightMargin( 0 ),
|
|
_shadowReceivingCoarseBoundAccuracy( BOUNDING_BOX )
|
|
{
|
|
|
|
}
|
|
|
|
MinimalShadowMap::MinimalShadowMap
|
|
(const MinimalShadowMap& copy, const osg::CopyOp& copyop) :
|
|
BaseClass(copy,copyop),
|
|
_maxFarPlane( copy._maxFarPlane ),
|
|
_minLightMargin( copy._minLightMargin ),
|
|
_shadowReceivingCoarseBoundAccuracy( copy._shadowReceivingCoarseBoundAccuracy )
|
|
{
|
|
}
|
|
|
|
MinimalShadowMap::~MinimalShadowMap()
|
|
{
|
|
}
|
|
|
|
osg::BoundingBox MinimalShadowMap::ViewData::computeShadowReceivingCoarseBounds()
|
|
{
|
|
// Default slowest but most precise
|
|
ShadowReceivingCoarseBoundAccuracy accuracy = DEFAULT_ACCURACY;
|
|
|
|
MinimalShadowMap * msm = dynamic_cast< MinimalShadowMap* >( _st.get() );
|
|
if( msm ) accuracy = msm->getShadowReceivingCoarseBoundAccuracy();
|
|
|
|
if( accuracy == MinimalShadowMap::EMPTY_BOX )
|
|
{
|
|
// One may skip coarse scene bounds computation if light is infinite.
|
|
// Empty box will be intersected with view frustum so in the end
|
|
// view frustum will be used as bounds approximation.
|
|
// But if light is nondirectional and bounds come out too large
|
|
// they may bring the effect of almost 180 deg perspective set
|
|
// up for shadow camera. Such projection will significantly impact
|
|
// precision of further math.
|
|
|
|
return osg::BoundingBox();
|
|
}
|
|
|
|
if( accuracy == MinimalShadowMap::BOUNDING_SPHERE )
|
|
{
|
|
// faster but less precise rough scene bound computation
|
|
// however if compute near far is active it may bring quite good result
|
|
osg::Camera * camera = _cv->getRenderStage()->getCamera();
|
|
osg::Matrix m = camera->getViewMatrix() * _clampedProjection;
|
|
|
|
ConvexPolyhedron frustum;
|
|
frustum.setToUnitFrustum();
|
|
frustum.transform( osg::Matrix::inverse( m ), m );
|
|
|
|
osg::BoundingSphere bs =_st->getShadowedScene()->getBound();
|
|
osg::BoundingBox bb;
|
|
bb.expandBy( bs );
|
|
osg::Polytope box;
|
|
box.setToBoundingBox( bb );
|
|
|
|
frustum.cut( box );
|
|
|
|
// approximate sphere with octahedron. Ie first cut by box then
|
|
// additionaly cut with the same box rotated 45, 45, 45 deg.
|
|
box.transform( // rotate box around its center
|
|
osg::Matrix::translate( -bs.center() ) *
|
|
osg::Matrix::rotate( osg::PI_4, 0, 0, 1 ) *
|
|
osg::Matrix::rotate( osg::PI_4, 1, 1, 0 ) *
|
|
osg::Matrix::translate( bs.center() ) );
|
|
frustum.cut( box );
|
|
|
|
return frustum.computeBoundingBox( );
|
|
}
|
|
|
|
if( accuracy == MinimalShadowMap::BOUNDING_BOX ) // Default
|
|
{
|
|
// more precise method but slower method
|
|
// bound visitor traversal takes lot of time for complex scenes
|
|
// (note that this adds to cull time)
|
|
|
|
osg::ComputeBoundsVisitor cbbv(osg::NodeVisitor::TRAVERSE_ACTIVE_CHILDREN);
|
|
cbbv.setTraversalMask(_st->getShadowedScene()->getCastsShadowTraversalMask());
|
|
_st->getShadowedScene()->osg::Group::traverse(cbbv);
|
|
|
|
return cbbv.getBoundingBox();
|
|
}
|
|
|
|
return osg::BoundingBox();
|
|
}
|
|
|
|
void MinimalShadowMap::ViewData::aimShadowCastingCamera(
|
|
const osg::BoundingSphere &bs,
|
|
const osg::Light *light,
|
|
const osg::Vec4 &lightPos,
|
|
const osg::Vec3 &lightDir,
|
|
const osg::Vec3 &lightUpVector
|
|
/* by default = osg::Vec3( 0, 1 0 )*/ )
|
|
{
|
|
BaseClass::ViewData::aimShadowCastingCamera( bs, light, lightPos, lightDir, lightUpVector );
|
|
}
|
|
|
|
void MinimalShadowMap::ViewData::aimShadowCastingCamera
|
|
( const osg::Light *light, const osg::Vec4 &lightPos,
|
|
const osg::Vec3 &lightDir, const osg::Vec3 &lightUp )
|
|
{
|
|
osg::BoundingBox bb = computeScenePolytopeBounds();
|
|
if( !bb.valid() ) { // empty scene or looking at the sky - substitute something
|
|
bb.expandBy( osg::BoundingSphere( _cv->getEyePoint(), 1 ) );
|
|
}
|
|
|
|
osg::Vec3 up = lightUp;
|
|
|
|
if( up.length2() <= 0 )
|
|
{
|
|
// This is extra step (not really needed but helpful in debuging)
|
|
// Compute such lightUp vector that shadow cam is intuitively aligned with eye
|
|
// We compute this vector on -ZY view plane, perpendicular to light direction
|
|
// Matrix m = ViewToWorld
|
|
#if 0
|
|
osg::Matrix m = osg::Matrix::inverse( _cv->getModelViewMatrix() );
|
|
osg::Vec3 camFw( -m( 2, 0 ), -m( 2, 1 ), -m( 2, 2 ) );
|
|
camFw.normalize();
|
|
|
|
osg::Vec3 camUp( m( 1, 0 ), m( 1, 1 ), m( 1, 2 ) );
|
|
camUp.normalize();
|
|
|
|
up = camUp * ( camFw * lightDir ) - camFw * ( camUp * lightDir );
|
|
up.normalize();
|
|
#else
|
|
osg::Matrix m = osg::Matrix::inverse( *_cv->getModelViewMatrix() );
|
|
// OpenGL std cam looks along -Z axis so Cam Fw = [ 0 0 -1 0 ] * m
|
|
up.set( -m( 2, 0 ), -m( 2, 1 ), -m( 2, 2 ) );
|
|
#endif
|
|
}
|
|
|
|
aimShadowCastingCamera( osg::BoundingSphere( bb ), light, lightPos, lightDir, up );
|
|
|
|
// Intersect scene Receiving Shadow Polytope with shadow camera frustum
|
|
// Important for cases where Scene extend beyond shadow camera frustum
|
|
// From this moment shadowed scene portion is fully contained by both
|
|
// main camera frustum and shadow camera frustum
|
|
osg::Matrix mvp = _camera->getViewMatrix() * _camera->getProjectionMatrix();
|
|
cutScenePolytope( osg::Matrix::inverse( mvp ), mvp );
|
|
|
|
frameShadowCastingCamera
|
|
( _cv->getRenderStage()->getCamera(), _camera.get(), 0 );
|
|
}
|
|
|
|
void MinimalShadowMap::ViewData::frameShadowCastingCamera
|
|
( const osg::Camera* cameraMain, osg::Camera* cameraShadow, int pass )
|
|
{
|
|
osg::Matrix mvp =
|
|
cameraShadow->getViewMatrix() * cameraShadow->getProjectionMatrix();
|
|
|
|
ConvexPolyhedron polytope = _sceneReceivingShadowPolytope;
|
|
std::vector<osg::Vec3d> points = _sceneReceivingShadowPolytopePoints;
|
|
|
|
osg::BoundingBox bb = computeScenePolytopeBounds( mvp );
|
|
|
|
// projection was trimmed above, need to recompute mvp
|
|
if( bb.valid() && *_minLightMarginPtr > 0 ) {
|
|
// bb._max += osg::Vec3( 1, 1, 1 );
|
|
// bb._min -= osg::Vec3( 1, 1, 1 );
|
|
|
|
osg::Matrix transform = osg::Matrix::inverse( mvp );
|
|
|
|
// Code below was working only for directional lights ie when projection was ortho
|
|
// osg::Vec3d normal = osg::Matrix::transform3x3( osg::Vec3d( 0,0,-1)., transfrom );
|
|
|
|
// So I replaced it with safer code working with spot lights as well
|
|
osg::Vec3d normal =
|
|
osg::Vec3d(0,0,-1) * transform - osg::Vec3d(0,0,1) * transform;
|
|
|
|
normal.normalize();
|
|
_sceneReceivingShadowPolytope.extrude( normal * *_minLightMarginPtr );
|
|
|
|
// Zero pass does crude shadowed scene hull approximation.
|
|
// Its important to cut it to coarse light frustum properly
|
|
// at this stage.
|
|
// If not cut and polytope extends beyond shadow projection clip
|
|
// space (-1..1), it may get "twisted" by precisely adjusted shadow cam
|
|
// projection in second pass.
|
|
|
|
if ( pass == 0 && _frameShadowCastingCameraPasses > 1 )
|
|
{ // Make sure extruded polytope does not extend beyond light frustum
|
|
osg::Polytope lightFrustum;
|
|
lightFrustum.setToUnitFrustum();
|
|
lightFrustum.transformProvidingInverse( mvp );
|
|
_sceneReceivingShadowPolytope.cut( lightFrustum );
|
|
}
|
|
|
|
_sceneReceivingShadowPolytopePoints.clear();
|
|
_sceneReceivingShadowPolytope.getPoints
|
|
( _sceneReceivingShadowPolytopePoints );
|
|
|
|
bb = computeScenePolytopeBounds( mvp );
|
|
}
|
|
|
|
setDebugPolytope( "extended",
|
|
_sceneReceivingShadowPolytope, osg::Vec4( 1, 0.5, 0, 1 ), osg::Vec4( 1, 0.5, 0, 0.1 ) );
|
|
|
|
_sceneReceivingShadowPolytope = polytope;
|
|
_sceneReceivingShadowPolytopePoints = points;
|
|
|
|
// Warning: Trim light projection at near plane may remove shadowing
|
|
// from objects outside of view space but still casting shadows into it.
|
|
// I have not noticed this issue so I left mask at default: all bits set.
|
|
if( bb.valid() )
|
|
trimProjection( cameraShadow->getProjectionMatrix(), bb, 1|2|4|8|16|32 );
|
|
|
|
///// Debuging stuff //////////////////////////////////////////////////////////
|
|
setDebugPolytope( "scene", _sceneReceivingShadowPolytope, osg::Vec4(0,1,0,1) );
|
|
|
|
|
|
#if PRINT_SHADOW_TEXEL_TO_PIXEL_ERROR
|
|
if( pass == 1 )
|
|
displayShadowTexelToPixelErrors
|
|
( cameraMain, cameraShadow, &_sceneReceivingShadowPolytope );
|
|
#endif
|
|
|
|
if( pass == _frameShadowCastingCameraPasses - 1 )
|
|
{
|
|
#if 1
|
|
{
|
|
osg::Matrix mvp = cameraShadow->getViewMatrix() * cameraShadow->getProjectionMatrix();
|
|
ConvexPolyhedron frustum;
|
|
frustum.setToUnitFrustum();
|
|
frustum.transform( osg::Matrix::inverse( mvp ), mvp );
|
|
|
|
setDebugPolytope( "shadowCamFrustum", frustum, osg::Vec4(0,0,1,1) );
|
|
}
|
|
|
|
{
|
|
osg::Matrix mvp = cameraMain->getViewMatrix() * cameraMain->getProjectionMatrix();
|
|
ConvexPolyhedron frustum;
|
|
frustum.setToUnitFrustum();
|
|
frustum.transform( osg::Matrix::inverse( mvp ), mvp );
|
|
|
|
setDebugPolytope( "mainCamFrustum", frustum, osg::Vec4(1,1,1,1) );
|
|
}
|
|
#endif
|
|
std::string * filename = getDebugDump( );
|
|
if( filename && !filename->empty() )
|
|
{
|
|
dump( *filename );
|
|
filename->clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
void MinimalShadowMap::ViewData::cullShadowReceivingScene( )
|
|
{
|
|
BaseClass::ViewData::cullShadowReceivingScene( );
|
|
|
|
_clampedProjection = *_cv->getProjectionMatrix();
|
|
|
|
if( _cv->getComputeNearFarMode() ) {
|
|
|
|
// Redo steps from CullVisitor::popProjectionMatrix()
|
|
// which clamps projection matrix when Camera & Projection
|
|
// completes traversal of their children
|
|
|
|
// We have to do this now manually
|
|
// because we did not complete camera traversal yet but
|
|
// we need to know how this clamped projection matrix will be
|
|
|
|
_cv->computeNearPlane();
|
|
|
|
osgUtil::CullVisitor::value_type n = _cv->getCalculatedNearPlane();
|
|
osgUtil::CullVisitor::value_type f = _cv->getCalculatedFarPlane();
|
|
|
|
if( n < f )
|
|
_cv->clampProjectionMatrix( _clampedProjection, n, f );
|
|
}
|
|
|
|
// Aditionally clamp far plane if shadows don't need to be cast as
|
|
// far as main projection far plane
|
|
if( 0 < *_maxFarPlanePtr )
|
|
clampProjection( _clampedProjection, 0.f, *_maxFarPlanePtr );
|
|
|
|
// Give derived classes chance to initialize _sceneReceivingShadowPolytope
|
|
osg::BoundingBox bb = computeShadowReceivingCoarseBounds( );
|
|
if( bb.valid() )
|
|
_sceneReceivingShadowPolytope.setToBoundingBox( bb );
|
|
else
|
|
_sceneReceivingShadowPolytope.clear();
|
|
|
|
// Cut initial scene using main camera frustum.
|
|
// Cutting will work correctly on empty polytope too.
|
|
// Take into consideration near far calculation and _maxFarPlane variable
|
|
|
|
|
|
osg::Matrix mvp = *_cv->getModelViewMatrix() * _clampedProjection;
|
|
|
|
cutScenePolytope( osg::Matrix::inverse( mvp ), mvp );
|
|
|
|
setDebugPolytope
|
|
( "frustum", _sceneReceivingShadowPolytope, osg::Vec4(1,0,1,1));
|
|
}
|
|
|
|
void MinimalShadowMap::ViewData::init( ThisClass *st, osgUtil::CullVisitor *cv )
|
|
{
|
|
BaseClass::ViewData::init( st, cv );
|
|
|
|
_modellingSpaceToWorldPtr = &st->_modellingSpaceToWorld;
|
|
_minLightMarginPtr = &st->_minLightMargin;
|
|
_maxFarPlanePtr = &st->_maxFarPlane;
|
|
|
|
_frameShadowCastingCameraPasses = 1;
|
|
}
|
|
|
|
void MinimalShadowMap::ViewData::cutScenePolytope
|
|
( const osg::Matrix & transform,
|
|
const osg::Matrix & inverse,
|
|
const osg::BoundingBox & bb )
|
|
{
|
|
_sceneReceivingShadowPolytopePoints.clear();
|
|
|
|
if( bb.valid() ) {
|
|
osg::Polytope polytope;
|
|
polytope.setToBoundingBox( bb );
|
|
polytope.transformProvidingInverse( inverse );
|
|
_sceneReceivingShadowPolytope.cut( polytope );
|
|
_sceneReceivingShadowPolytope.getPoints
|
|
( _sceneReceivingShadowPolytopePoints );
|
|
} else
|
|
_sceneReceivingShadowPolytope.clear();
|
|
}
|
|
|
|
osg::BoundingBox
|
|
MinimalShadowMap::ViewData::computeScenePolytopeBounds( const osg::Matrix & m )
|
|
{
|
|
osg::BoundingBox bb;
|
|
|
|
if( &m )
|
|
for( unsigned i = 0; i < _sceneReceivingShadowPolytopePoints.size(); ++i )
|
|
bb.expandBy( _sceneReceivingShadowPolytopePoints[i] * m );
|
|
else
|
|
for( unsigned i = 0; i < _sceneReceivingShadowPolytopePoints.size(); ++i )
|
|
bb.expandBy( _sceneReceivingShadowPolytopePoints[i] );
|
|
|
|
return bb;
|
|
}
|
|
|
|
|
|
|
|
// Utility methods for adjusting projection matrices
|
|
|
|
void MinimalShadowMap::ViewData::trimProjection
|
|
( osg::Matrixd & projectionMatrix, osg::BoundingBox bb, unsigned int trimMask )
|
|
{
|
|
#if 1
|
|
if( !bb.valid() || !( trimMask & (1|2|4|8|16|32) ) ) return;
|
|
double l = -1, r = 1, b = -1, t = 1, n = 1, f = -1;
|
|
|
|
#if 0
|
|
// make sure bounding box does not extend beyond unit frustum clip range
|
|
for( int i = 0; i < 3; i ++ ) {
|
|
if( bb._min[i] < -1 ) bb._min[i] = -1;
|
|
if( bb._max[i] > 1 ) bb._max[i] = 1;
|
|
}
|
|
#endif
|
|
|
|
if( trimMask & 1 ) l = bb._min[0];
|
|
if( trimMask & 2 ) r = bb._max[0];
|
|
if( trimMask & 4 ) b = bb._min[1];
|
|
if( trimMask & 8 ) t = bb._max[1];
|
|
if( trimMask & 16 ) n = -bb._min[2];
|
|
if( trimMask & 32 ) f = -bb._max[2];
|
|
|
|
projectionMatrix.postMult( osg::Matrix::ortho( l,r,b,t,n,f ) );
|
|
#else
|
|
if( !bb.valid() || !( trimMask & (1|2|4|8|16|32) ) ) return;
|
|
double l, r, t, b, n, f;
|
|
bool ortho = projectionMatrix.getOrtho( l, r, b, t, n, f );
|
|
if( !ortho && !projectionMatrix.getFrustum( l, r, b, t, n, f ) )
|
|
return; // rotated or skewed or other crooked projection - give up
|
|
|
|
// make sure bounding box does not extend beyond unit frustum clip range
|
|
for( int i = 0; i < 3; i ++ ) {
|
|
if( bb._min[i] < -1 ) bb._min[i] = -1;
|
|
if( bb._max[i] > 1 ) bb._max[i] = 1;
|
|
}
|
|
|
|
osg::Matrix projectionToView = osg::Matrix::inverse( projectionMatrix );
|
|
|
|
osg::Vec3 min =
|
|
osg::Vec3( bb._min[0], bb._min[1], bb._min[2] ) * projectionToView;
|
|
|
|
osg::Vec3 max =
|
|
osg::Vec3( bb._max[0], bb._max[1], bb._max[2] ) * projectionToView;
|
|
|
|
if( trimMask & 16 ) { // trim near
|
|
if( !ortho ) { // recalc frustum corners on new near plane
|
|
l *= -min[2] / n;
|
|
r *= -min[2] / n;
|
|
b *= -min[2] / n;
|
|
t *= -min[2] / n;
|
|
}
|
|
n = -min[2];
|
|
}
|
|
|
|
if( trimMask & 32 ) // trim far
|
|
f = -max[2];
|
|
|
|
if( !ortho ) {
|
|
min[0] *= -n / min[2];
|
|
min[1] *= -n / min[2];
|
|
max[0] *= -n / max[2];
|
|
max[1] *= -n / max[2];
|
|
}
|
|
|
|
if( l < r ) { // check for inverted X range
|
|
if( l < min[0] && ( trimMask & 1 ) ) l = min[0];
|
|
if( r > max[0] && ( trimMask & 2 ) ) r = max[0];
|
|
} else {
|
|
if( l > min[0] && ( trimMask & 1 ) ) l = min[0];
|
|
if( r < max[0] && ( trimMask & 2 ) ) r = max[0];
|
|
}
|
|
|
|
if( b < t ) { // check for inverted Y range
|
|
if( b < min[1] && ( trimMask & 4 ) ) b = min[1];
|
|
if( t > max[1] && ( trimMask & 8 ) ) t = max[1];
|
|
} else {
|
|
if( b > min[1] && ( trimMask & 4 ) ) b = min[1];
|
|
if( t < max[1] && ( trimMask & 8 ) ) t = max[1];
|
|
}
|
|
|
|
if( ortho )
|
|
projectionMatrix.makeOrtho( l, r, b, t, n, f );
|
|
else
|
|
projectionMatrix.makeFrustum( l, r, b, t, n, f );
|
|
#endif
|
|
}
|
|
|
|
void MinimalShadowMap::ViewData::clampProjection
|
|
( osg::Matrixd & projection, float new_near, float new_far )
|
|
{
|
|
double r, l, t, b, n, f;
|
|
bool perspective = projection.getFrustum( l, r, b, t, n, f );
|
|
if( !perspective && !projection.getOrtho( l, r, b, t, n, f ) )
|
|
{
|
|
// What to do here ?
|
|
OSG_WARN << "MinimalShadowMap::clampProjectionFarPlane failed - non standard matrix" << std::endl;
|
|
|
|
} else if( n < new_near || new_far < f ) {
|
|
|
|
if( n < new_near && new_near < f ) {
|
|
if( perspective ) {
|
|
l *= new_near / n;
|
|
r *= new_near / n;
|
|
b *= new_near / n;
|
|
t *= new_near / n;
|
|
}
|
|
n = new_near;
|
|
}
|
|
|
|
if( n < new_far && new_far < f ) {
|
|
f = new_far;
|
|
}
|
|
|
|
if( perspective )
|
|
projection.makeFrustum( l, r, b, t, n, f );
|
|
else
|
|
projection.makeOrtho( l, r, b, t, n, f );
|
|
}
|
|
}
|
|
|
|
// Imagine following scenario:
|
|
// We stand in the room and look through the window.
|
|
// How should our view change if we were looking through larger window ?
|
|
// In other words how should projection be adjusted if
|
|
// window had grown by some margin ?
|
|
// Method computes such new projection which maintains perpective/world ratio
|
|
|
|
void MinimalShadowMap::ViewData::extendProjection
|
|
( osg::Matrixd & projection, osg::Viewport * viewport, const osg::Vec2& margin )
|
|
{
|
|
double l,r,b,t,n,f;
|
|
|
|
//osg::Matrix projection = camera.getProjectionMatrix();
|
|
|
|
bool frustum = projection.getFrustum( l,r,b,t,n,f );
|
|
|
|
if( !frustum && !projection.getOrtho( l,r,b,t,n,f ) ) {
|
|
OSG_WARN << " Awkward projection matrix. ComputeExtendedProjection failed" << std::endl;
|
|
return;
|
|
}
|
|
|
|
osg::Matrix window = viewport->computeWindowMatrix();
|
|
|
|
osg::Vec3 vMin( viewport->x() - margin.x(),
|
|
viewport->y() - margin.y(),
|
|
0.0 );
|
|
|
|
osg::Vec3 vMax( viewport->width() + margin.x() * 2 + vMin.x(),
|
|
viewport->height() + margin.y() * 2 + vMin.y(),
|
|
0.0 );
|
|
|
|
osg::Matrix inversePW = osg::Matrix::inverse( projection * window );
|
|
|
|
vMin = vMin * inversePW;
|
|
vMax = vMax * inversePW;
|
|
|
|
l = vMin.x();
|
|
r = vMax.x();
|
|
b = vMin.y();
|
|
t = vMax.y();
|
|
|
|
if( frustum )
|
|
projection.makeFrustum( l,r,b,t,n,f );
|
|
else
|
|
projection.makeOrtho( l,r,b,t,n,f );
|
|
}
|
|
|