Files
WizarDiego-NodeRig/frontend/lensCamera.js
T

515 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// LENS CAMERA v1.0 — Simulação de Lentes Óticas para o NodeRig
// Módulo independente: sem modificações em app.js
// ============================================================
(function () {
"use strict";
// ============================================================
// ESTADO
// ============================================================
var focalLength = 50; // mm (padrão: 50mm "nifty fifty")
var distortionK = 0.0; // barrel distortion (0 = nenhum)
var blurAmount = 0.0; // bokeh amount (0–1)
var vignetteAmount = 0.0; // vignette (0–1)
var caAmount = 0.0; // chromatic aberration (0–1)
var lensMode = "normal";
var isPostEnabled = false;
var renderTarget = null;
var postScene = null;
var postCamera = null;
var postMaterial = null;
var _origRender = null;
// Presets de focal (mm)
var FOCAL_PRESETS = [14, 24, 35, 50, 85, 135, 200];
// Presets por modo de lente
var LENS_MODES = {
normal: { focal: 50, distortion: 0.00 },
angular: { focal: 24, distortion: 0.10 },
fisheye: { focal: 14, distortion: 0.42 }
};
// ============================================================
// CÁLCULOS ÓPTICOS
// ============================================================
/**
* Converte distância focal (mm) em FOV vertical (graus)
* usando sensor full-frame 35mm (36×24mm).
*/
function fovFromFocal(mm) {
var sensorH = 24.0; // mm (altura do sensor)
return 2.0 * Math.atan(sensorH / (2.0 * mm)) * (180.0 / Math.PI);
}
/**
* Converte valor do slider de blur (0–1) em f-stop equivalente.
* 0 = f/16 (fechado / nítido), 1 = f/1.2 (aberto / bokeh).
*/
function blurToFstop(v) {
var logMin = Math.log(1.2);
var logMax = Math.log(16.0);
var logF = logMax - v * (logMax - logMin);
return "f/" + Math.exp(logF).toFixed(1);
}
// ============================================================
// SHADERS GLSL
// ============================================================
var VERT = [
"varying vec2 vUv;",
"void main() {",
" vUv = uv;",
" gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);",
"}"
].join("\n");
var FRAG = [
"uniform sampler2D tDiffuse;",
"uniform float uDistortion;",
"uniform float uBlur;",
"uniform float uVignette;",
"uniform float uCA;",
"varying vec2 vUv;",
"",
"// Barrel / fisheye distortion",
"vec2 barrel(vec2 uv, float k) {",
" vec2 c = uv * 2.0 - 1.0;",
" float r2 = dot(c, c);",
" c *= 1.0 + k * r2 + k * 0.15 * r2 * r2;",
" return c * 0.5 + 0.5;",
"}",
"",
"void main() {",
" vec2 dUv = (uDistortion > 0.001) ? barrel(vUv, uDistortion) : vUv;",
"",
" // Fora dos limites → preto (crop circular do fisheye)",
" if (dUv.x < 0.0 || dUv.x > 1.0 || dUv.y < 0.0 || dUv.y > 1.0) {",
" gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);",
" return;",
" }",
"",
" // 12-tap Poisson disk bokeh",
" float br = uBlur * 0.065;",
" vec4 color;",
" if (br > 0.0001) {",
" color = texture2D(tDiffuse, dUv + br * vec2(-0.326,-0.406));",
" color += texture2D(tDiffuse, dUv + br * vec2(-0.840,-0.074));",
" color += texture2D(tDiffuse, dUv + br * vec2(-0.696, 0.457));",
" color += texture2D(tDiffuse, dUv + br * vec2(-0.203, 0.621));",
" color += texture2D(tDiffuse, dUv + br * vec2( 0.962,-0.195));",
" color += texture2D(tDiffuse, dUv + br * vec2( 0.473,-0.480));",
" color += texture2D(tDiffuse, dUv + br * vec2( 0.519, 0.767));",
" color += texture2D(tDiffuse, dUv + br * vec2( 0.185,-0.893));",
" color += texture2D(tDiffuse, dUv + br * vec2( 0.507, 0.064));",
" color += texture2D(tDiffuse, dUv + br * vec2( 0.896, 0.412));",
" color += texture2D(tDiffuse, dUv + br * vec2(-0.322,-0.933));",
" color += texture2D(tDiffuse, dUv + br * vec2(-0.792, 0.716));",
" color /= 12.0;",
" } else {",
" color = texture2D(tDiffuse, dUv);",
" }",
"",
" // Aberração cromática — desfasagem radial RGB",
" if (uCA > 0.001) {",
" vec2 toCenter = 0.5 - dUv;",
" float ca = uCA * 0.022;",
" color.r = texture2D(tDiffuse, dUv - toCenter * ca).r;",
" color.b = texture2D(tDiffuse, dUv + toCenter * ca).b;",
" }",
"",
" // Vinheta",
" if (uVignette > 0.001) {",
" float d = length(vUv * 2.0 - 1.0);",
" color.rgb *= 1.0 - smoothstep(0.25, 1.35, d) * uVignette;",
" }",
"",
" gl_FragColor = color;",
"}"
].join("\n");
// ============================================================
// POST-PROCESS
// ============================================================
function getRTSize() {
return {
w: renderer.domElement.width || window.innerWidth,
h: renderer.domElement.height || window.innerHeight
};
}
function initPostProcess() {
if (renderTarget) return;
var sz = getRTSize();
renderTarget = new THREE.WebGLRenderTarget(sz.w, sz.h, {
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
format: THREE.RGBAFormat
});
postCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
postScene = new THREE.Scene();
postMaterial = new THREE.ShaderMaterial({
vertexShader: VERT,
fragmentShader: FRAG,
uniforms: {
tDiffuse: { value: renderTarget.texture },
uDistortion: { value: 0.0 },
uBlur: { value: 0.0 },
uVignette: { value: 0.0 },
uCA: { value: 0.0 }
},
depthTest: false,
depthWrite: false
});
var PlaneGeo = THREE.PlaneGeometry || THREE.PlaneBufferGeometry;
postScene.add(new THREE.Mesh(new PlaneGeo(2, 2), postMaterial));
// Intercepta renderer.render — zero-cost quando desativado
_origRender = renderer.render.bind(renderer);
renderer.render = function (sc, cam) {
if (isPostEnabled) {
renderer.setRenderTarget(renderTarget);
_origRender(sc, cam);
renderer.setRenderTarget(null);
_origRender(postScene, postCamera);
} else {
_origRender(sc, cam);
}
};
console.log("[LensCamera] Post-process pronto: " + sz.w + "×" + sz.h);
}
function updateRTSize() {
if (!renderTarget) return;
var sz = getRTSize();
renderTarget.setSize(sz.w, sz.h);
}
function syncUniforms() {
if (!postMaterial) return;
postMaterial.uniforms.uDistortion.value = distortionK;
postMaterial.uniforms.uBlur.value = blurAmount;
postMaterial.uniforms.uVignette.value = vignetteAmount;
postMaterial.uniforms.uCA.value = caAmount;
}
function checkPostProcess() {
isPostEnabled = distortionK > 0.001 || blurAmount > 0.001 ||
vignetteAmount > 0.001 || caAmount > 0.001;
if (isPostEnabled && !renderTarget) initPostProcess();
if (renderTarget) syncUniforms();
}
// ============================================================
// APLICA FOCAL / MODO
// ============================================================
function applyFocalLength(mm) {
focalLength = Math.max(14, Math.min(200, mm));
camera.fov = fovFromFocal(focalLength);
camera.updateProjectionMatrix();
refreshUI();
}
function applyLensMode(mode) {
lensMode = mode;
var p = LENS_MODES[mode];
distortionK = p.distortion;
applyFocalLength(p.focal);
// Sliders
setSliderVal("lens-focal-slider", p.focal);
setSliderVal("lens-distortion-slider", p.distortion);
// Distortion row: ocultar no modo Normal
var distRow = document.getElementById("lens-distortion-row");
if (distRow) distRow.style.display = (mode === "normal") ? "none" : "";
// Botões de tipo
["normal", "angular", "fisheye"].forEach(function (m) {
toggleClass("lens-type-" + m, "active", m === mode);
});
checkPostProcess();
refreshUI();
}
// ============================================================
// ATUALIZA DISPLAY
// ============================================================
function refreshUI() {
var fov = fovFromFocal(focalLength);
setText("lens-focal-val", focalLength + "mm • " + fov.toFixed(1) + "°");
setText("lens-distortion-val", distortionK.toFixed(2));
setText("lens-blur-val", blurToFstop(blurAmount) + " (" + blurAmount.toFixed(2) + ")");
setText("lens-vignette-val", vignetteAmount.toFixed(2));
setText("lens-ca-val", caAmount.toFixed(2));
var modeLabel = { normal: "Normal", angular: "Angular", fisheye: "Fisheye" };
setText("lens-header-info", focalLength + "mm · " + (modeLabel[lensMode] || ""));
// Chips de focal
FOCAL_PRESETS.forEach(function (mm) {
toggleClass("lens-preset-" + mm, "active", Math.abs(focalLength - mm) < 1);
});
}
// ============================================================
// RESET
// ============================================================
function resetLens() {
focalLength = 50;
distortionK = 0.0;
blurAmount = 0.0;
vignetteAmount = 0.0;
caAmount = 0.0;
lensMode = "normal";
isPostEnabled = false;
applyFocalLength(50);
var sliders = {
"lens-focal-slider": 50,
"lens-distortion-slider": 0,
"lens-blur-slider": 0,
"lens-vignette-slider": 0,
"lens-ca-slider": 0
};
Object.keys(sliders).forEach(function (id) { setSliderVal(id, sliders[id]); });
["normal", "angular", "fisheye"].forEach(function (m) {
toggleClass("lens-type-" + m, "active", m === "normal");
});
var distRow = document.getElementById("lens-distortion-row");
if (distRow) distRow.style.display = "none";
if (postMaterial) syncUniforms();
refreshUI();
if (typeof showStatus === "function") showStatus("Lente resetada para padrão (50mm)");
}
// ============================================================
// PAINEL DE CONTROLE
// ============================================================
function buildPresetChips() {
return FOCAL_PRESETS.map(function (mm) {
var active = mm === 50 ? " active" : "";
return '<button class="lens-preset-btn' + active + '" id="lens-preset-' + mm +
'" data-focal="' + mm + '">' + mm + '</button>';
}).join("");
}
function createLensPanel() {
if (document.getElementById("lens-panel")) return;
var panel = document.createElement("div");
panel.id = "lens-panel";
panel.className = "lens-panel";
panel.innerHTML =
// ── Header ──────────────────────────────────────────
'<div class="lens-header">' +
'<span class="lens-title">📷 Lente</span>' +
'<span class="lens-header-info" id="lens-header-info">50mm · Normal</span>' +
'</div>' +
// ── Distância Focal ──────────────────────────────────
'<div class="lens-section">' +
'<div class="lens-section-label">Distância Focal</div>' +
'<div class="lens-presets">' + buildPresetChips() + '</div>' +
'<div class="lens-slider-group">' +
'<input type="range" id="lens-focal-slider" class="lens-slider lens-slider-focal"' +
' min="14" max="200" step="1" value="50">' +
'<span class="lens-value" id="lens-focal-val">50mm • 19.0°</span>' +
'</div>' +
'</div>' +
// ── Tipo de Lente ─────────────────────────────────────
'<div class="lens-section">' +
'<div class="lens-section-label">Tipo de Lente</div>' +
'<div class="lens-type-row">' +
'<button class="lens-type-btn active" id="lens-type-normal">📷 Normal</button>' +
'<button class="lens-type-btn" id="lens-type-angular">◻ Angular</button>' +
'<button class="lens-type-btn" id="lens-type-fisheye">◉ Fisheye</button>' +
'</div>' +
'</div>' +
// ── Distorção (oculta no modo Normal) ────────────────
'<div class="lens-section" id="lens-distortion-row" style="display:none;">' +
'<div class="lens-slider-group">' +
'<div class="lens-slider-header">' +
'<span class="lens-slider-label">Distorção Radial</span>' +
'<span class="lens-value" id="lens-distortion-val">0.00</span>' +
'</div>' +
'<input type="range" id="lens-distortion-slider" class="lens-slider lens-slider-blue"' +
' min="0" max="0.8" step="0.01" value="0">' +
'</div>' +
'</div>' +
// ── Bokeh / Abertura ──────────────────────────────────
'<div class="lens-section">' +
'<div class="lens-slider-group">' +
'<div class="lens-slider-header">' +
'<span class="lens-slider-label">Abertura (Bokeh)</span>' +
'<span class="lens-value" id="lens-blur-val">f/16 (0.00)</span>' +
'</div>' +
'<div class="lens-blur-labels"><span>f/16 Sharp</span><span>f/1.2 Aberto</span></div>' +
'<input type="range" id="lens-blur-slider" class="lens-slider lens-slider-amber"' +
' min="0" max="1" step="0.01" value="0">' +
'</div>' +
'</div>' +
// ── Vinheta ───────────────────────────────────────────
'<div class="lens-section">' +
'<div class="lens-slider-group">' +
'<div class="lens-slider-header">' +
'<span class="lens-slider-label">Vinheta</span>' +
'<span class="lens-value" id="lens-vignette-val">0.00</span>' +
'</div>' +
'<input type="range" id="lens-vignette-slider" class="lens-slider lens-slider-purple"' +
' min="0" max="1" step="0.01" value="0">' +
'</div>' +
'</div>' +
// ── Aberração Cromática ───────────────────────────────
'<div class="lens-section">' +
'<div class="lens-slider-group">' +
'<div class="lens-slider-header">' +
'<span class="lens-slider-label">Aberração Cromática</span>' +
'<span class="lens-value" id="lens-ca-val">0.00</span>' +
'</div>' +
'<input type="range" id="lens-ca-slider" class="lens-slider lens-slider-ca"' +
' min="0" max="1" step="0.01" value="0">' +
'</div>' +
'</div>' +
// ── Reset ─────────────────────────────────────────────
'<div class="lens-section lens-section-reset">' +
'<button class="lens-reset-btn" id="lens-reset-btn">↺ Reset Lente</button>' +
'</div>';
document.body.appendChild(panel);
wireListeners(panel);
}
function wireListeners(panel) {
// Focal presets
FOCAL_PRESETS.forEach(function (mm) {
on("lens-preset-" + mm, "click", function () { applyFocalLength(mm); });
});
// Focal slider
on("lens-focal-slider", "input", function () {
applyFocalLength(parseInt(this.value, 10));
});
// Lens type buttons
["normal", "angular", "fisheye"].forEach(function (mode) {
on("lens-type-" + mode, "click", function () { applyLensMode(mode); });
});
// Distortion slider
on("lens-distortion-slider", "input", function () {
distortionK = parseFloat(this.value);
setText("lens-distortion-val", distortionK.toFixed(2));
checkPostProcess();
});
// Blur slider
on("lens-blur-slider", "input", function () {
blurAmount = parseFloat(this.value);
checkPostProcess();
refreshUI();
});
// Vignette slider
on("lens-vignette-slider", "input", function () {
vignetteAmount = parseFloat(this.value);
checkPostProcess();
setText("lens-vignette-val", vignetteAmount.toFixed(2));
});
// CA slider
on("lens-ca-slider", "input", function () {
caAmount = parseFloat(this.value);
checkPostProcess();
setText("lens-ca-val", caAmount.toFixed(2));
});
// Reset
on("lens-reset-btn", "click", resetLens);
// Bloqueia propagação de eventos para dock/orbit
panel.addEventListener("pointerdown", function (e) { e.stopPropagation(); });
panel.addEventListener("mousedown", function (e) { e.stopPropagation(); });
// Atualiza RenderTarget quando resolução ou janela mudam
on("res-selector", "change", function () { setTimeout(updateRTSize, 150); });
window.addEventListener("resize", function () { setTimeout(updateRTSize, 150); });
}
// ============================================================
// HELPERS DOM
// ============================================================
function on(id, evt, fn) {
var el = document.getElementById(id);
if (el) el.addEventListener(evt, fn);
}
function setText(id, text) {
var el = document.getElementById(id);
if (el) el.textContent = text;
}
function setSliderVal(id, val) {
var el = document.getElementById(id);
if (el) el.value = val;
}
function toggleClass(id, cls, state) {
var el = document.getElementById(id);
if (el) el.classList.toggle(cls, state);
}
// ============================================================
// API GLOBAL
// ============================================================
window.LensCamera = {
setFocal: applyFocalLength,
setMode: applyLensMode,
reset: resetLens,
isActive: function () { return isPostEnabled; },
getFocal: function () { return focalLength; },
getFOV: function () { return fovFromFocal(focalLength); }
};
// ============================================================
// INICIALIZAÇÃO
// ============================================================
function init() {
createLensPanel();
applyFocalLength(50); // Padrão: 50mm
console.log("[LensCamera] Sistema de lentes v1.0 pronto.");
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
setTimeout(init, 250); // Após app.js + skeletonRig.js
}
})();