대규모 3D 씬에서 그림자 계산은 성능에 큰 영향을 미칩니다.
동적 그림자 캐싱을 통해 이를 최적화해보겠습니다.
class ShadowCacheSystem {
constructor(scene) {
this.scene = scene;
this.shadowCache = new Map();
this.staticObjects = new Set();
this.dynamicObjects = new Set();
this.lastUpdateTime = {};
// 그림자 맵 해상도 설정
this.shadowMapSize = this.calculateOptimalShadowMapSize();
}
calculateOptimalShadowMapSize() {
// 디바이스 성능에 따른 최적 그림자 맵 크기 계산
const gpu = renderer.capabilities;
const maxSize = gpu.maxTextureSize;
const memory = navigator.deviceMemory || 4; // 기본값 4GB
if (memory >= 8 && maxSize >= 4096) return 2048;
if (memory >= 4 && maxSize >= 2048) return 1024;
return 512;
}
addStaticObject(object) {
object.castShadow = true;
object.receiveShadow = true;
this.staticObjects.add(object);
// 정적 오브젝트의 그림자 맵 미리 계산
this.precalculateShadows(object);
}
precalculateShadows(object) {
const shadowTexture = new THREE.WebGLRenderTarget(
this.shadowMapSize,
this.shadowMapSize,
{
format: THREE.RGBAFormat,
type: THREE.FloatType
}
);
// 그림자 맵 생성
const shadowCamera = new THREE.OrthographicCamera(-10, 10, 10, -10, 0.5, 500);
this.scene.lights.forEach(light => {
this.renderShadowMap(object, light, shadowCamera, shadowTexture);
});
this.shadowCache.set(object.uuid, shadowTexture);
}
renderShadowMap(object, light, camera, target) {
const currentXform = object.matrixWorld.clone();
const lightDirection = light.position.clone().sub(object.position).normalize();
camera.position.copy(object.position).add(lightDirection.multiplyScalar(20));
camera.lookAt(object.position);
renderer.setRenderTarget(target);
renderer.render(this.scene, camera);
renderer.setRenderTarget(null);
object.matrixWorld.copy(currentXform);
}
update(deltaTime) {
// 동적 오브젝트 그림자 업데이트
this.dynamicObjects.forEach(object => {
if (this.shouldUpdateShadow(object, deltaTime)) {
this.updateDynamicShadow(object);
}
});
}
shouldUpdateShadow(object, deltaTime) {
const lastUpdate = this.lastUpdateTime[object.uuid] || 0;
const timeSinceUpdate = deltaTime - lastUpdate;
// 이동 거리에 따른 업데이트 필요성 확인
const movement = object.position.distanceTo(object.userData.lastPosition || object.position);
const shouldUpdate = movement > 0.1 || timeSinceUpdate > 1000;
if (shouldUpdate) {
object.userData.lastPosition = object.position.clone();
this.lastUpdateTime[object.uuid] = deltaTime;
}
return shouldUpdate;
}
}
사용자의 디바이스 성능과 씬의 복잡도에 따라 그림자 품질을 자동으로 조정하는 시스템입니다.
class ShadowQualityManager {
constructor(renderer, scene) {
this.renderer = renderer;
this.scene = scene;
this.fpsHistory = [];
this.qualityLevels = {
high: {
type: THREE.PCFSoftShadowMap,
mapSize: 2048,
bias: -0.0001,
radius: 4
},
medium: {
type: THREE.PCFShadowMap,
mapSize: 1024,
bias: -0.001,
radius: 2
},
low: {
type: THREE.BasicShadowMap,
mapSize: 512,
bias: -0.002,
radius: 1
}
};
this.currentQuality = 'medium';
this.setupInitialQuality();
}
setupInitialQuality() {
// GPU 성능 체크
const gpu = this.renderer.capabilities;
const maxTexSize = gpu.maxTextureSize;
if (maxTexSize >= 4096 && this.checkHighPerformance()) {
this.setQuality('high');
} else if (maxTexSize >= 2048) {
this.setQuality('medium');
} else {
this.setQuality('low');
}
}
checkHighPerformance() {
// WebGL 확장 기능 체크
const gl = this.renderer.getContext();
const extensions = [
'EXT_color_buffer_float',
'OES_texture_float_linear'
];
return extensions.every(ext => gl.getExtension(ext));
}
setQuality(level) {
const quality = this.qualityLevels[level];
this.renderer.shadowMap.type = quality.type;
this.scene.traverse(object => {
if (object.isLight && object.shadow) {
object.shadow.mapSize.width = quality.mapSize;
object.shadow.mapSize.height = quality.mapSize;
object.shadow.bias = quality.bias;
object.shadow.radius = quality.radius;
// 그림자 맵 리셋
object.shadow.map = null;
}
});
this.currentQuality = level;
}
updateFPSHistory(fps) {
this.fpsHistory.push(fps);
if (this.fpsHistory.length > 60) {
this.fpsHistory.shift();
}
const avgFPS = this.fpsHistory.reduce((a, b) => a + b) / this.fpsHistory.length;
this.adjustQualityBasedOnFPS(avgFPS);
}
adjustQualityBasedOnFPS(avgFPS) {
if (avgFPS < 30 && this.currentQuality !== 'low') {
this.setQuality('low');
console.log('그림자 품질 저하: 성능 최적화를 위해 품질을 낮춥니다.');
} else if (avgFPS > 55 && this.currentQuality === 'low') {
this.setQuality('medium');
console.log('그림자 품질 향상: 성능이 충분하여 품질을 높입니다.');
}
}
}
부드러운 그림자 효과를 위한 커스텀 후처리 시스템입니다.
class AdvancedShadowBlur {
constructor(renderer, scene, camera) {
this.renderer = renderer;
this.scene = scene;
this.camera = camera;
this.setupRenderTargets();
this.setupShaderMaterials();
}
setupRenderTargets() {
const shadowSize = 1024;
this.shadowRenderTarget = new THREE.WebGLRenderTarget(
shadowSize, shadowSize, {
format: THREE.RGBAFormat,
type: THREE.FloatType
}
);
this.blurTargets = [
new THREE.WebGLRenderTarget(shadowSize, shadowSize),
new THREE.WebGLRenderTarget(shadowSize, shadowSize)
];
}
setupShaderMaterials() {
// 가우시안 블러 쉐이더
this.blurMaterial = new THREE.ShaderMaterial({
uniforms: {
tShadow: { value: null },
resolution: { value: new THREE.Vector2(1024, 1024) },
blurSize: { value: 2.0 }
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D tShadow;
uniform vec2 resolution;
uniform float blurSize;
varying vec2 vUv;
void main() {
vec4 sum = vec4(0.0);
vec2 texelSize = vec2(1.0 / resolution.x, 1.0 / resolution.y);
// 9x9 가우시안 블러 커널
for(float x = -4.0; x <= 4.0; x++) {
for(float y = -4.0; y <= 4.0; y++) {
vec2 offset = vec2(x, y) * texelSize * blurSize;
float weight = exp(-(x*x + y*y) / (2.0 * 8.0)) / (2.0 * 3.14159 * 8.0);
sum += texture2D(tShadow, vUv + offset) * weight;
}
}
gl_FragColor = sum;
}
`
});
}
render() {
// 원본 그림자 렌더링
this.renderer.setRenderTarget(this.shadowRenderTarget);
this.renderer.render(this.scene, this.camera);
// 수평 블러
this.blurMaterial.uniforms.tShadow.value = this.shadowRenderTarget.texture;
this.blurMaterial.uniforms.resolution.value.set(1024, 1024);
this.renderBlurPass(this.blurTargets[0]);
// 수직 블러
this.blurMaterial.uniforms.tShadow.value = this.blurTargets[0].texture;
this.renderBlurPass(this.blurTargets[1]);
// 최종 결과를 화면에 렌더링
this.renderer.setRenderTarget(null);
this.renderer.render(this.scene, this.camera);
}
renderBlurPass(target) {
this.renderer.setRenderTarget(target);
this.renderer.render(this.scene, this.camera);
}
}
거리에 따라 그림자 품질을 자동으로 조절하는 시스템입니다.
class ShadowLODSystem {
constructor(camera) {
this.camera = camera;
this.lodLevels = new Map();
this.distanceThresholds = [10, 30, 50]; // 거리 기준점
}
addObject(object, shadowConfigs) {
this.lodLevels.set(object, {
high: {
mapSize: new THREE.Vector2(2048, 2048),
bias: -0.0001,
radius: 4,
...shadowConfigs?.high
},
medium: {
mapSize: new THREE.Vector2(1024, 1024),
bias: -0.001,
radius: 2,
...shadowConfigs?.medium
},
low: {
mapSize: new THREE.Vector2(512, 512),
bias: -0.002,
radius: 1,
...shadowConfigs?.low
}
});
}
update() {
this.lodLevels.forEach((configs, object) => {
const distance = this.camera.position.distanceTo(object.position);
const level = this.calculateLODLevel(distance);
this.applyShadowConfig(object, configs[level]);
});
}
calculateLODLevel(distance) {
if (distance < this.distanceThresholds[0]) return 'high';
if (distance < this.distanceThresholds[1]) return 'medium';
return 'low';
}
applyShadowConfig(object, config) {
if (object.shadow) {
object.shadow.mapSize.copy(config.mapSize);
object.shadow.bias = config.bias;
object.shadow.radius = config.radius;
// 변경사항이 있을 때만 그림자 맵 업데이트
if (object.shadow.needsUpdate) {
object.shadow.map = null;
object.shadow.needsUpdate = false;
}
}
}
}
이러한 시스템들을 통합하여 사용하는 방법입니다.
class ShadowDebugger {
constructor(renderer, scene) {
this.renderer = renderer;
this.scene = scene;
this.debugModes = {
NONE: 'none',
SHADOW_MAP: 'shadowMap',
SHADOW_CASCADE: 'shadowCascade',
PERFORMANCE: 'performance'
};
this.currentMode = this.debugModes.NONE;
this.stats = {
drawCalls: 0,
shadowMapTime: 0,
totalTime: 0
};
this.setupDebugDisplay();
}
setupDebugDisplay() {
this.debugContainer = document.createElement('div');
Object.assign(this.debugContainer.style, {
position: 'fixed',
top: '10px',
right: '10px',
backgroundColor: 'rgba(0, 0, 0, 0.7)',
color: 'white',
padding: '10px',
fontFamily: 'monospace',
fontSize: '12px',
zIndex: 1000
});
document.body.appendChild(this.debugContainer);
}
startMeasuring() {
this.measurementStart = performance.now();
return () => {
const end = performance.now();
return end - this.measurementStart;
};
}
analyzeShadowMaps(lights) {
const analysis = [];
lights.forEach(light => {
if (light.shadow && light.shadow.map) {
const shadowMap = light.shadow.map;
analysis.push({
type: light.type,
mapSize: shadowMap.width + 'x' + shadowMap.height,
memoryUsage: this.calculateTextureMemory(shadowMap),
renderTime: this.measureShadowMapRenderTime(light)
});
}
});
return analysis;
}
calculateTextureMemory(shadowMap) {
const pixelCount = shadowMap.width * shadowMap.height;
// 깊이 텍스처는 일반적으로 32비트 float 사용
return (pixelCount * 4) / (1024 * 1024); // MB 단위
}
measureShadowMapRenderTime(light) {
const endMeasurement = this.startMeasuring();
// 임시 렌더 타겟으로 그림자 맵 렌더링
const currentRenderTarget = this.renderer.getRenderTarget();
this.renderer.setRenderTarget(light.shadow.map);
this.renderer.render(this.scene, light.shadow.camera);
this.renderer.setRenderTarget(currentRenderTarget);
return endMeasurement();
}
updateDebugDisplay() {
if (this.currentMode === this.debugModes.NONE) {
this.debugContainer.style.display = 'none';
return;
}
this.debugContainer.style.display = 'block';
let content = '';
switch (this.currentMode) {
case this.debugModes.SHADOW_MAP:
content = this.formatShadowMapDebug();
break;
case this.debugModes.SHADOW_CASCADE:
content = this.formatShadowCascadeDebug();
break;
case this.debugModes.PERFORMANCE:
content = this.formatPerformanceDebug();
break;
}
this.debugContainer.innerHTML = content;
}
formatShadowMapDebug() {
const lights = this.scene.lights || [];
const analysis = this.analyzeShadowMaps(lights);
return `
<h3>Shadow Map Debug</h3>
${analysis.map(info => `
<div>
<p>Light Type: ${info.type}</p>
<p>Map Size: ${info.mapSize}</p>
<p>Memory Usage: ${info.memoryUsage.toFixed(2)} MB</p>
<p>Render Time: ${info.renderTime.toFixed(2)} ms</p>
</div>
`).join('<hr>')}
`;
}
formatShadowCascadeDebug() {
const directionalLights = this.scene.lights?.filter(light =>
light.type === 'DirectionalLight' && light.shadow?.cascades
) || [];
return `
<h3>Shadow Cascade Debug</h3>
${directionalLights.map(light => `
<div>
<p>Light Name: ${light.name || 'Unnamed'}</p>
<p>Cascade Count: ${light.shadow.cascades.length}</p>
${light.shadow.cascades.map((cascade, i) => `
<div>
<p>Cascade ${i + 1}:</p>
<p>Range: ${cascade.near.toFixed(2)} - ${cascade.far.toFixed(2)}</p>
<p>Resolution: ${cascade.mapSize.width}x${cascade.mapSize.height}</p>
</div>
`).join('')}
</div>
`).join('<hr>')}
`;
}
formatPerformanceDebug() {
return `
<h3>Shadow Performance</h3>
<p>Draw Calls: ${this.stats.drawCalls}</p>
<p>Shadow Map Time: ${this.stats.shadowMapTime.toFixed(2)} ms</p>
<p>Total Render Time: ${this.stats.totalTime.toFixed(2)} ms</p>
<p>Shadow/Total Ratio: ${((this.stats.shadowMapTime / this.stats.totalTime) * 100).toFixed(1)}%</p>
`;
}
setDebugMode(mode) {
if (this.debugModes[mode]) {
this.currentMode = this.debugModes[mode];
this.updateDebugDisplay();
}
}
update() {
if (this.currentMode === this.debugModes.NONE) return;
// 성능 측정 업데이트
const endMeasurement = this.startMeasuring();
// 그림자 맵 렌더링 시간 측정
const lights = this.scene.lights || [];
let shadowTime = 0;
lights.forEach(light => {
if (light.shadow) {
shadowTime += this.measureShadowMapRenderTime(light);
}
});
this.stats.shadowMapTime = shadowTime;
this.stats.totalTime = endMeasurement();
this.stats.drawCalls = this.renderer.info?.render?.calls || 0;
this.updateDebugDisplay();
}
}