@@ -50,7 +50,7 @@ public class RealLensOpenGlFrameCapturer
final float GOLDEN_ANGLE = ( float ) ( Math . PI * ( 3 . 0 - Math . sqrt ( 5 . 0 ) ) ) ;
float random_offset = ( float ) Math . sqrt ( r * r / n ) * 0 . 5 f;
float random_offset = ( float ) Math . sqrt ( r * r / n ) * 0 . 0 f;
for ( int i = 0 ; i < n ; i + + ) {
float rr = r * ( float ) Math . sqrt ( ( float ) i / n ) ;
@@ -88,6 +88,24 @@ public class RealLensOpenGlFrameCapturer
return cameras ;
}
private static Data singleDiskSample ( int i , int n , float focalDistance , float apertureRadius ) {
final float GOLDEN_ANGLE = ( float ) ( Math . PI * ( 3 . 0 - Math . sqrt ( 5 . 0 ) ) ) ;
float rr = apertureRadius * ( float ) Math . sqrt ( ( float ) i / n ) ;
float t = i * GOLDEN_ANGLE ;
float dx = rr * ( float ) Math . cos ( t ) ;
float dy = rr * ( float ) Math . sin ( t ) ;
float dz = 0f ;
Data cam = new Data ( dx , dy , dz , 0f , 0f ) ;
float dirX = - dx , dirY = - dy , dirZ = focalDistance - dz ;
cam . yaw = ( float ) - Math . toDegrees ( Math . atan2 ( dirX , dirZ ) ) ;
cam . pitch = ( float ) Math . toDegrees ( Math . atan2 ( dirY , Math . sqrt ( dirX * dirX + dirZ * dirZ ) ) ) ;
return cam ;
}
private final Path positionPath ;
private final Timeline timeline ;
private final ReplayHandler replayHandler ;
@@ -112,61 +130,65 @@ public class RealLensOpenGlFrameCapturer
RenderSettings settings = renderInfo . getRenderSettings ( ) ;
int fps = settings . getFramesPerSecond ( ) ;
int apertureSamples = settings . getLensBlurSamples ( ) ;
int subframes = Math . max ( 1 , settings . getMotionBlurSubframes ( ) ) ; // M
int N = Math . max ( 1 , settings . getLensBlurSamples ( ) ) ; // единый бюджет сэмплов
float shutter = settings . getShutterAngle ( ) ; // градусы
boolean motionBlur = shutter > 0f ;
double dt = 1000 . 0 / fps ; // длительность выходного кадра, мс
// сколько с у б -кадров затвор открыт (округляем до целого)
int openCount = Math . max ( 1 , ( int ) Math . round ( subframes * shutter / 360 . 0 ) ) ;
double shutterFrac = motionBlur ? ( shutter / 360 . 0 ) : 0 . 0 ;
int width = getFrameWidth ( ) ;
int height = getFrameHeight ( ) ;
int bpp = 4 ;
int totalBytes = width * height * bpp ;
// ОДИН аккумулятор на весь выходной кадр (сырой BGRA-порядок из glReadPixels)
long [ ] accumulator = new long [ totalBytes ] ;
int totalSamples = 0 ;
for ( int j = 0 ; j < openCount ; j + + ) {
// абсолютное время с у б -кадра на таймлайне (мс), только вперёд
long tSub = ( long ) ( frameId * dt + ( j / ( double ) subframes ) * dt ) ;
// перемешанные слои времени, чтобы время не коррелировало с индексом апертурной спирали
int [ ] order = new int [ N ] ;
for ( int i = 0 ; i < N ; i + + ) order [ i ] = i ;
for ( int i = N - 1 ; i > 0 ; i - - ) { // Fisher-Yates
int j = ThreadLocalRandom . current ( ) . nextInt ( i + 1 ) ;
int tmp = order [ i ] ;
order [ i ] = order [ j ] ;
order [ j ] = tmp ;
}
// пересэмплить камеру + сущности + FOV/линзу на с у б -времени
for ( int i = 0 ; i < N ; i + + ) {
// --- время: 1 стратифицированный джиттернутый сэмпл на слой ---
long tSub ;
if ( motionBlur ) {
double u = ( i + ThreadLocalRandom . current ( ) . nextDouble ( ) ) / N ;
tSub = ( long ) ( frameId * dt + u * shutterFrac * dt ) ;
timeline . applyToGame ( tSub , replayHandler ) ;
} else {
tSub = ( long ) ( frameId * dt ) ;
}
// partialTicks от РЕАЛЬНОГО времени реплея (frac(replayTime/50)),
// как в AbstractTimelinePlayer.onTick. (tSub % 50) даёт рассинхрон -> раздвоение.
long replayTime = replayHandler . getReplaySender ( ) . currentTimeStamp ( ) ;
float subPartialTicks = ( float ) ( ( replayTime % 50L ) / 50 . 0 ) ;
// partialTicks для интерполяции сущностей внутри тика (50 мс)
float subPartialTicks = ( float ) ( ( tSub % 50L ) / 5 0. 0 ) ;
float focal = positionPath . getValue ( LensProperties . FOCAL_DISTANCE , tSub ) . map ( Triple : : getLeft ) . orElse ( 5 . 0f ) ;
float apert = positionPath . getValue ( LensProperties . APERTURE_RADIUS , tSub ) . map ( Triple : : getLeft ) . orElse ( 0 . 05f ) ;
Data cam = singleDiskSample ( order [ i ] , N , focal , apert ) ;
// DOF-параметры берём тоже на с у б -времени, чтобы фокус ехал во время смаза
float focalDistance = positionPath . getValue ( LensProperties . FOCAL_DISTANCE , tSub ) . map ( Triple : : getLeft ) . orElse ( 5 . 0f ) ;
float apertureRadius = positionPath . getValue ( LensProperties . APERTURE_RADIUS , tSub ) . map ( Triple : : getLeft ) . orElse ( 0 . 05f ) ;
Data [ ] cameras = calculateCameras ( focalDistance , apertureRadius , apertureSamples ) ;
for ( Data cam : cameras ) {
OpenGlFrame sub = renderFrame ( frameId , subPartialTicks , cam ) ;
ByteBuffer buf = sub . getByteBuffer ( ) ;
buf . rewind ( ) ;
for ( int i = 0 ; i < totalBytes ; i + + ) {
accumulator [ i ] + = ( buf . get ( i ) & 0xFF ) ;
}
ByteBufferPool . release ( buf ) ; // сразу освобождаем -> нет OOM
totalSamples + + ;
for ( int k = 0 ; k < totalBytes ; k + + ) {
accumulator [ k ] + = ( buf . get ( k ) & 0xFF ) ;
}
ByteBufferPool . release ( buf ) ;
}
// усредняем в один кадр
ByteBuffer out = ByteBufferPool . allocate ( totalBytes ) ;
for ( int i = 0 ; i < totalBytes ; i + + ) {
out . put ( ( byte ) ( accumulator [ i ] / totalSamples ) ) ;
for ( int k = 0 ; k < totalBytes ; k + + ) {
out . put ( ( byte ) ( accumulator [ k ] / N ) ) ;
}
out . rewind ( ) ;
OpenGlFrame averaged = new OpenGlFrame ( frameId , new Dimension ( width , height ) , bpp , out ) ;
// Оборачиваем в RealLensOpenGlFrame с ОДНИМ кадром — процессор е г о пропустит через
// openGlBytesToBitmap (flip + swap R/B) и поделит на 1. Результат идентичен старому DOF.
return Collections . singletonMap ( Channel . BRGA , new RealLensOpenGlFrame ( new OpenGlFrame [ ] { averaged } ) ) ;
}
}