<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<title>3D 几何渲染系统(无限数量+参数实时生效+光线追踪)</title>
<style>
body { margin: 0; background: #000; display: flex; flex-direction: column; align-items: center; font-family: Arial; overflow: hidden; }
#canvas { background: #111; border: 1px solid #333; }
.controls { position: fixed; top: 10px; right: 10px; color: #fff; display: flex; flex-direction: column; gap: 10px; padding: 10px; background: rgba(0,0,0,0.7); border-radius: 8px; }
input { width: 80px; padding: 5px; border: none; border-radius: 4px; }
button { padding: 6px 12px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #45a049; }
#fps { position: fixed; top: 10px; left: 10px; color: #00ff00; font-size: 16px; font-weight: bold; }
#materialTips { position: fixed; top: 40px; left: 10px; color: #ff6666; font-size: 14px; background: rgba(0,0,0,0.8); padding: 5px; border-radius: 4px; }
</style>
</head>
<body>
<div id="fps">FPS: 0</div>
<div id="materialTips">1=反光 2=镜子 3=透光 4=折射 | 亮度系数: <span id="brightnessVal">1.0</span></div>
<canvas id="canvas"></canvas>
<div class="controls">
<label>物体数量: <input type="number" id="objCount" value="8" min="1" max="999" step="1"></label>
<label>缩放系数: <input type="number" id="scale" value="1.0" min="0.1" max="5.0" step="0.1"></label>
<label>物体间距: <input type="number" id="gap" value="3.0" min="0.5" max="20.0" step="0.5"></label>
<label>物体尺寸: <input type="number" id="objSize" value="0.7" min="0.1" max="3.0" step="0.1"></label>
<label>全局亮度: <input type="number" id="brightness" value="1.0" min="0.1" max="2.0" step="0.1"></label>
<button id="startBtn">启动/刷新渲染</button>
</div>
<script>
function F(x, k) { return 1 + Math.pow(x, k) + x; }
function integral(eq, a, b, d=0.1, K=5) {
const g = new Function('x', `return ${eq};`);
let s = [[a, b]], r = 0.0;
while (s.length > 0) {
let n = [];
for (let [x0, x1] of s) {
const dx = x1 - x0;
if (dx < d) {
const m = (x0 + x1)/2;
const m_scaled = m/(2*Math.PI);
const f_values = Array.from({length:K}, (_,k) => F(m_scaled, k+1));
let W = 0.0, V = 0.0;
const g_m = g(m);
for (let k = 0; k < K; k++) {
const w = 1/(f_values[k] + 1e-12);
W += w; V += w * g_m;
}
r += dx * (W ? V/W : g_m);
continue;
}
const u = g(x0), v = g(x1), mid = (x0+x1)/2, mv = g(mid);
if (Math.abs(u-v) < 1e-8 && Math.abs(mv-u) < 1e-8) {
r += dx * mv; continue;
}
const h = dx/10;
for (let i = 0; i < 10; i++) n.push([x0+i*h, x0+(i+1)*h]);
}
s = n;
}
return r;
}
function c(seg) { let cnt = 0; for (let i=0;i<seg.length;i++) for(let j=i+1;j<seg.length;j++) if(seg[i]>seg[j]) cnt++; return cnt; }
function w(arr, wa) {
let a = [...arr], n = a.length;
if (n <=1) return a;
const exp_wa = Array.from({length:n}, (_,i) => Math.pow(wa, i+1));
for (let i=0;i<n;i++) {
let swapped = false;
for (let j=0;j<n-i-1;j++) {
const v1 = a[j]*(1 + wa + exp_wa[j]);
const v2 = a[j+1]*(1 + wa + exp_wa[j+1]);
if (Math.abs(v1-v2) <1e-8) {
const eq = `{a\[j\]}\*x - {a[j+1]}*x`;
const r = integral(eq, 0, 1);
if (r>0) { [a[j],a[j+1]] = [a[j+1],a[j]]; swapped=true; }
} else if (v1>v2) { [a[j],a[j+1]] = [a[j+1],a[j]]; swapped=true; }
}
if (!swapped) break;
}
return a;
}
function sortAngle(arr) {
let n = arr.length;
if (n<=1) return [...arr];
let segs = [], i=0;
while (i < n) {
let l=1;
while (i+l <n && arr[i+l]>=arr[i+l-1]) l++;
const s = arr.slice(i, i+l);
const r = l>1 ? c(s)/(l-1) : 0;
segs.push({s, r});
i += l;
}
let n_segs = [];
for (let {s, r} of segs) {
if (r>0 && s.length>1) {
const avg = s.reduce((a,b)=>a+b)/s.length;
const wa_sum = Array.from({length:20}, (_,k) => 1/(F(avg, k+1)+1e-12)).reduce((a,b)=>a+b);
n_segs.push({s:w(s, wa_sum), r: c(w(s, wa_sum))/(s.length-1)});
} else n_segs.push({s, r});
}
return n_segs.flatMap(item => item.s);
}
const canvas = document.getElementById('canvas');const ctx = canvas.getContext('2d');
let [W, H] = [window.innerWidth, window.innerHeight];
canvas.width = W; canvas.height = H;
let objects = [];
let isRendering = false;
let rotateX = 0, rotateY = 0;
let lastTime = 0, frameCount = 0, fps = 0;
let globalBrightness = 1.0;
// 全局参数(实时更新)
let params = {
count: 8,
scale: 1.0,
gap: 3.0,
size: 0.7
};
// 材质类型定义
const MATERIAL_TYPES = {
REFLECTIVE: 1, // 反光
MIRROR: 2, // 镜子
TRANSLUCENT:3, // 透光
REFRACTIVE:4 // 折射
};
// 生成均匀分布位置(适配任意数量,避免超出视角)
function getUniformPos(index, count, gap) {
const gridSize = Math.ceil(Math.sqrt(count));
const normX = (index % gridSize) / gridSize - 0.5;
const normY = Math.floor(index / gridSize) / gridSize - 0.5;
const x = normX * gridSize * gap;
const y = normY * gridSize * gap;
const z = Math.random()*gap*2 + 3;
return {x, y, z};
}
// 计算高光:基于物体位置和方向,生成局部高光区域
function calcHighlight(obj, faceVertices, lightDir) {
const center = faceVertices.reduce((acc, v) => ({
x: acc.x + v.x / faceVertices.length,
y: acc.y + v.y / faceVertices.length
}), {x:0, y:0});
const highlightRadius = obj.size * 15 * params.scale;
const highlightIntensity = obj.material === MATERIAL_TYPES.MIRROR ? 0.8 :
obj.material === MATERIAL_TYPES.REFLECTIVE ? 0.5 : 0;
if (highlightIntensity <= 0) return 0;
// 基于旋转角度(转弯幅度)计算高光衰减
const rotateFactor = Math.abs(rotateX) + Math.abs(rotateY);
const attenuation = 1 / (1 + rotateFactor * 0.1);
return highlightIntensity * attenuation * globalBrightness;
}
// HSL转RGB(用于调整饱和度和亮度)
function hslToRgb(h, s, l) {
s = Math.max(0, Math.min(1, s));
l = Math.max(0, Math.min(1, l));
let r, g, b;
if (s === 0) r = g = b = l;
else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) };
}
// 完整几何体类(新增材质、高光、明暗饱和度联动)
class SolidGeometry {
constructor(id, pos, type, size) {
this.id = id;
this.pos = pos;
this.type = type;
this.size = size;
this.baseHue = (id*30) % 360;
this.material = Math.floor(Math.random() * 4) + 1; // 随机材质
this.color = `hsl(${this.baseHue}, 70%, 50%)`;
this.vertices = this.generateVertices();
this.faces = this.generateFaces();
this.edges = this.generateEdges();
}
generateVertices() {
const s = this.size;
if (this.type === 'cube') {
return [
-s,-s,-s\], \[s,-s,-s\], \[s,s,-s\], \[-s,s,-s\], \[-s,-s,s\], \[s,-s,s\], \[s,s,s\], \[-s,s,s
];
} else {
const vertexCount = Math.floor(Math.random()*6)+4;
const vertices = [];
for (let i=0; i<vertexCount; i++) {
const angle = (i/vertexCount)*Math.PI*2;
const x = Math.cos(angle)*s;
const y = Math.sin(angle)*s;
const z = (Math.random()-0.5)*s*2;
vertices.push([x, y, z]);
}
return vertices;
}
}
generateFaces() {
if (this.type === 'cube') {
return [[0,1,2,3], [4,5,6,7], [0,1,5,4], [1,2,6,5], [2,3,7,6], [3,0,4,7]];
} else {
const faces = [];
for (let i=1; i<this.vertices.length-1; i++) {
faces.push([0, i, i+1]);
}
return faces;
}
}
generateEdges() {
if (this.type === 'cube') {
return [[0,1],[1,2],[2,3],[3,0],[4,5],[5,6],[6,7],[7,4],[0,4],[1,5],[2,6],[3,7]];
} else {
const edges = [];
for (let i=0; i<this.vertices.length; i++) {
edges.push([i, (i+1)%this.vertices.length]);
}
for (let i=1; i<this.vertices.length-1; i++) {
edges.push([0, i]);
}
return edges;
}
}
projectVertex(v) {
let [x, y, z] = v;
x += this.pos.x; y += this.pos.y; z += this.pos.z;
x *= params.scale; y *= params.scale; z *= params.scale;
const cosX = Math.cos(rotateX), sinX = Math.sin(rotateX);
const cosY = Math.cos(rotateY), sinY = Math.sin(rotateY);
let ny = y*cosX - z*sinX;
let nz = y*sinX + z*cosX;
let nx = x*cosY + nz*sinY;
nz = -x*sinY + nz*cosY;
const fov = 250;
const projX = (nx * fov) / (nz + 20) + W/2;
const projY = (ny * fov) / (nz + 20) + H/2;
return {x: projX, y: projY, z: nz};
}
render() {
const projected = this.vertices.map(v => this.projectVertex(v));
// 旋转幅度(转弯幅度):越大越暗
const rotateAmplitude = Math.abs(rotateX) + Math.abs(rotateY);
const brightnessFactor = 1 / (1 + rotateAmplitude * 0.2) * globalBrightness;
// 饱和度系数:旋转幅度越大,饱和度越低
const saturationFactor = 1 / (1 + rotateAmplitude * 0.15);
this.faces.forEach((face, faceIdx) => {
const faceVertices = face.map(idx => projected[idx]);
// 计算面的高光强度
const lightDir = {x: Math.cos(rotateY), y: Math.sin(rotateX)};
const highlight = calcHighlight(this, faceVertices, lightDir);
// 根据材质和旋转幅度调整颜色
let h = this.baseHue / 360;
let s = 0.7 * saturationFactor;
let l = 0.5 * brightnessFactor;
// 材质特殊效果
if (this.material === MATERIAL_TYPES.MIRROR) {
s = 0.9; l = l * 1.2 + highlight; // 镜子高饱和高亮度
} else if (this.material === MATERIAL_TYPES.TRANSLUCENT) {
l = l * 0.8 + highlight * 0.3; // 透光半透明
} else if (this.material === MATERIAL_TYPES.REFRACTIVE) {
s = s * 0.8; l = l * 1.1; // 折射轻微去饱和
} else if (this.material === MATERIAL_TYPES.REFLECTIVE) {
l = l + highlight; // 反光叠加高光
}
const rgb = hslToRgb(h, s, l);
const faceColor = `rgb({rgb.r}, {rgb.g}, ${rgb.b})`;
// 绘制面:添加高光渐变(局部区域高光)
const gradient = ctx.createRadialGradient(
faceVertices[0].x, faceVertices[0].y, 0,
faceVertices[0].x, faceVertices[0].y, this.size * 20 * params.scale
);
gradient.addColorStop(0, `rgba(255,255,255,${highlight})`);
gradient.addColorStop(1, faceColor);
ctx.fillStyle = this.material === MATERIAL_TYPES.REFLECTIVE || this.material === MATERIAL_TYPES.MIRROR ? gradient : faceColor;
ctx.beginPath();
ctx.moveTo(faceVertices[0].x, faceVertices[0].y);
faceVertices.forEach(v => ctx.lineTo(v.x, v.y));
ctx.closePath();
ctx.fill();
});
// 绘制边
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 1.5;
this.edges.forEach(edge => {
const v1 = projected[edge[0]];
const v2 = projected[edge[1]];
ctx.beginPath();
ctx.moveTo(v1.x, v1.y);
ctx.lineTo(v2.x, v2.y);
ctx.stroke();
});
// 绘制材质标记(红色数字)
const center = projected.reduce((acc, v) => ({
x: acc.x + v.x / projected.length,
y: acc.y + v.y / projected.length
}), {x:0, y:0});
ctx.fillStyle = '#ff0000';
ctx.font = 'bold 16px Arial';
ctx.textAlign = 'center';
ctx.fillText(this.material.toString(), center.x, center.y);
}
}
// 光线采样优化
function optimizeRays() {
const angles = Array.from({length:100}, () => Math.random()*Math.PI*2);
return sortAngle(angles).slice(0, 20);
}
// 重建物体列表(参数修改后调用)
function rebuildObjects() {
objects = [];
for (let i=0; i<params.count; i++) {
const pos = getUniformPos(i, params.count, params.gap);
const type = Math.random() > 0.5 ? 'cube' : 'poly';
objects.push(new SolidGeometry(i, pos, type, params.size));
}
}
// 动画循环
function animate(currentTime) {
if (!isRendering) return;
requestAnimationFrame(animate);
frameCount++;
if (currentTime - lastTime >= 1000) {
fps = frameCount;
frameCount = 0;
lastTime = currentTime;
document.getElementById('fps').textContent = `FPS: ${fps}`;
}
ctx.clearRect(0, 0, W, H);
optimizeRays();
// 按物体 z 轴排序(整体遮挡)
objects.sort((a,b) => {
const aZ = a.pos.z * params.scale;
const bZ = b.pos.z * params.scale;
return aZ - bZ;
});
objects.forEach(obj => obj.render());
}
// 初始化控制(参数实时监听+更新)
function initControls() {
document.getElementById('objCount').addEventListener('input', (e) => params.count = parseInt(e.target.value));
document.getElementById('scale').addEventListener('input', (e) => params.scale = parseFloat(e.target.value));
document.getElementById('gap').addEventListener('input', (e) => params.gap = parseFloat(e.target.value));
document.getElementById('objSize').addEventListener('input', (e) => params.size = parseFloat(e.target.value));
document.getElementById('brightness').addEventListener('input', (e) => {
globalBrightness = parseFloat(e.target.value);
document.getElementById('brightnessVal').textContent = globalBrightness.toFixed(1);
});
// 鼠标/触屏旋转
let isDragging = false;
let lastX, lastY;
canvas.addEventListener('mousedown', (e) => { isDragging = true; [lastX, lastY] = [e.clientX, e.clientY]; });
canvas.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = e.clientX - lastX;
const dy = e.clientY - lastY;
rotateY += dx * 0.01;
rotateX += dy * 0.01;
lastX, lastY\] = \[e.clientX, e.clientY\]; }); canvas.addEventListener('mouseup', () =\> isDragging = false); canvas.addEventListener('touchstart', (e) =\> { isDragging = true; \[lastX, lastY\] = \[e.touches\[0\].clientX, e.touches\[0\].clientY\]; }); canvas.addEventListener('touchmove', (e) =\> { if (!isDragging) return; const dx = e.touches\[0\].clientX - lastX; const dy = e.touches\[0\].clientY - lastY; rotateY += dx \* 0.01; rotateX += dy \* 0.01; \[lastX, lastY\] = \[e.touches\[0\].clientX, e.touches\[0\].clientY\]; }); canvas.addEventListener('touchend', () =\> isDragging = false); // 启动/刷新按钮 document.getElementById('startBtn').addEventListener('click', () =\> { isRendering = true; rebuildObjects(); requestAnimationFrame(animate); }); } // 窗口自适应 window.addEventListener('resize', () =\> { \[W, H\] = \[window.innerWidth, window.innerHeight\]; canvas.width = W; canvas.height = H; }); initControls(); \ \