水波干涉互动课件

这个程序要实现的是沪科版选择性必修一第二章 第四节 机械波的干涉和衍射中的图 3–28。如下图所示。

图 3–28  两列相同波相遇

做出的成品如下所示(注意:二维圆形水波的振幅应该随半径的增大而减小,否则违反能量守恒定律,但这样做的话可视性不佳,因此该课件没有体现振幅衰减):

核心代码解读

这个程序主要要解决三个问题:表示波峰、谷的圆的绘制,表示振动加强、减弱区域的双曲线的绘制,干涉图样的绘制,其中前两个问题主要涉及的是几何知识。

一、表示波峰、谷的圆的绘制

波速是由介质决定的,因此本程序的波速 v = 100(像素/秒)保持不变,而两列波的频率可以改变,可由 λ = \(\dfrac{v}{f}\) 求得两列波的波长。在动画代码中由 r = vt 求出波前圆的半径,缩小 λ/4 波长后绘制出最外层的波峰圆,颜色为黑色。然后持续缩小 λ/4,依次绘制波谷(蓝色)、波峰(黑色)……。代码如下:

// 计算波峰和波谷
function calculateWaveCrestsAndTroughs(time, speed, wavelength, phaseOffset) {
    if (time <= 0) {
        return [];
    }
    const distance = speed * time;
    const phaseShift = phaseOffset * wavelength / (2 * Math.PI);
    const waveCircles = [];

    let i = 0;
    while (true) {
        // 波峰比波前小四分之一波长
        const rawRadius = distance + phaseShift - i * (wavelength / 2) - wavelength / 4;

        if (rawRadius <= 0) {
            break;
        }

        if (rawRadius < params.maxRadius) {
            const type = i % 2 === 0 ? 'crest' : 'trough';
            waveCircles.push({
                radius: rawRadius,
                type: type
            });
        }

        i++;
    }

    return waveCircles;
}

二、表示振动加强、减弱区域的双曲线的绘制

写起来比较复杂,让 AI 帮我写吧:

1.核心算法:从物理条件到几何图形

代码严格遵循了干涉加强/减弱的物理定义来生成曲线,而不是简单地画圆找交点。

物理条件(体现在 drawSingleHyperbola 函数中):

2.关键函数:getPointOnHyperbola 的作用

这是一个通用的数学工具函数,负责生成标准双曲线上的点,并将其旋转平移到实际位置。

处理流程:

3.绘制逻辑:drawHyperbola 的执行步骤

第一步:参数计算

第二步:循环绘制

第三步:特殊处理:中垂线(n = 0且无相位差)

当计算出的路程差 pathDiff 非常接近 0 时(对应 n = 0 且相位差也为 0 的情况),双曲线会退化为一条直线——中垂线。

代码会计算焦点连线的垂直向量,然后绘制一条长长的直线来代替双曲线,避免了 a = 0 时双曲线计算失效的问题。

第四步:左右分支的处理

代码通过判断 pathDiff 的符号来决定绘制哪一侧的双曲线:

三、干涉图样的绘制

由物理知识:如下图所示,设两列波 S1、S2 的角频率为 ω,振幅分别为 A1A2,初相位分别 φ1φ2,它们发出的波在空间某点 P 相遇,在 P 点引起的振动分别为

\[{y_1} = {A_1}\sin (\omega t + {\varphi _1} - \frac{{2\pi {r_1}}}{\lambda })\]

\[{y_2} = {A_2}\sin (\omega t + {\varphi _2} - \frac{{2\pi {r_2}}}{\lambda })\]

式中 r1r2 为波源到 P 点的距离,则 P 的合位移为

\[y = {y_1} + {y_2}\]

空间中某点 P 位移的计算

还可以进一步推导出 P 的合振动(程序中没有用到)为:

\[y = A\sin (\omega t + \varphi )\]

式中

\[A = \sqrt {A_1^2 + A_2^2 + 2{A_1}{A_2}\cos \Delta \varphi } \]

\[\tan \varphi  = \frac{{{A_1}\sin ({\varphi _1} - \frac{{2\pi {r_1}}}{\lambda }) + {A_2}\sin ({\varphi _2} - \frac{{2\pi {r_2}}}{\lambda })}}{{{A_1}\cos ({\varphi _1} - \frac{{2\pi {r_1}}}{\lambda }) + {A_2}\cos ({\varphi _2} - \frac{{2\pi {r_2}}}{\lambda })}}\]

Δφ = φ2φ1 − \({\dfrac{{2\pi ({r_2} - {r_1})}}{\lambda }}\) 中的 φ2φ1 是波源的初相位差:− \({\dfrac{{2\pi ({r_2} - {r_1})}}{\lambda }}\) 是由于波程差(Δr = r1r2)引起的相位差。两者的代数和 Δφ 就是两列波传播到 P 点后,在该点振动的总相位差

干涉图样是绘制在一个分辨率为 1400×1080 的画布上的,在程序中,每帧(1/60 秒)时间内 CPU 都需要遍历约 150 万个像素,计算像素到波源的距离,然后代入上面的行波方程求出合位移,最后还需要将位移值映射到颜色,可视化干涉图样。核心代码如下:

// Canvas 2D渲染函数
function drawShadowCanvas2D() {
    const ctx = canvas2d.getContext('2d');
    if (!ctx) {
        console.warn('无法获取Canvas 2D上下文');
        return;
    }
    // 使用canvas的实际大小
    const width = canvas2d.width;
    const height = canvas2d.height;
    const imageData = ctx.createImageData(width, height);
    const data = imageData.data;

    const wavelength1 = params.waveSpeed / params.frequency1;
    const wavelength2 = params.waveSpeed / params.frequency2;
    const k1 = 2 * Math.PI / wavelength1;
    const k2 = 2 * Math.PI / wavelength2;
    const omega1 = 2 * Math.PI * params.frequency1;
    const omega2 = 2 * Math.PI * params.frequency2;
    const phaseOffset1 = 0;
    const phaseOffset2 = params.phaseDiff * Math.PI;

    const source1X = sources[0].x;
    const source1Y = sources[0].y;
    const source2X = sources[1].x;
    const source2Y = sources[1].y;

    const svgSource1X = parseFloat(sources[0].element.getAttribute('cx'));
    const svgSource1Y = parseFloat(sources[0].element.getAttribute('cy'));
    const svgSource2X = parseFloat(sources[1].element.getAttribute('cx'));
    const svgSource2Y = parseFloat(sources[1].element.getAttribute('cy'));

    // 计算相位偏移对应的距离偏移
    const phaseShift1 = phaseOffset1 * wavelength1 / (2 * Math.PI);
    const phaseShift2 = phaseOffset2 * wavelength2 / (2 * Math.PI);

    // 计算波已经传播的最大距离(包含相位偏移)
    const maxDistance1 = params.waveSpeed * params.time + phaseShift1;
    const maxDistance2 = params.waveSpeed * params.time + phaseShift2;

    const step = 1;

    // 使用SVG坐标系(原点在左上角)进行遍历和计算
    for (let svgY = 0; svgY < height; svgY += step) {
        for (let x = 0; x < width; x += step) {
            // Canvas 2D像素坐标(原点在左上角)
            const r1 = Math.sqrt(Math.pow(x - source1X, 2) + Math.pow(svgY -
            const r2 = Math.sqrt(Math.pow(x - source2X, 2) + Math.pow(svgY -

            // 只有当波已经传播到该点时才计算位移
            let displacement1 = 0;
            let displacement2 = 0;

            if (r1 <= maxDistance1) {
            const phase1 = omega1 * params.time - k1 * r1 + phaseOffset1;
            displacement1 = params.amplitude1 * Math.sin(phase1);
        }

        if (r2 <= maxDistance2) {
            const phase2 = omega2 * params.time - k2 * r2 + phaseOffset2;
            displacement2 = params.amplitude2 * Math.sin(phase2);
        }

        let r, g, b;
        // 如果两个波都还没传播到该点,使用位移为0时的颜色
        if (r1 > maxDistance1 && r2 > maxDistance2) {
            // 位移为0时的颜色
            if (params.shadowColorMode === 'high-contrast') {
            r = 0; g = 0; b = 0;
        } else {
            r = 128; g = 128; b = 128;
        }
        } else {
            const totalDisplacement = displacement1 + displacement2;

        if (params.shadowColorMode === 'high-contrast') {
            
            if (totalDisplacement > 0) {
                const t = totalDisplacement / 4;
                r = Math.floor(0 + 94 * t);
                g = Math.floor(0 + 163 * t);
                b = Math.floor(0 + 255 * t);
            } else if (totalDisplacement < 0) {
                const t = -totalDisplacement / 4;
                r = Math.floor(0 + 255 * t);
                g = Math.floor(0 + 165 * t);
                b = Math.floor(0 + 0 * t);
            } else {
                r = 0; g = 0; b = 0;
            }
            } else {
                // 灰度渐变模式
                if (totalDisplacement > 0) {
                    const t = totalDisplacement / 4;
                    r = Math.floor(128 + 127 * t);
                    g = Math.floor(128 + 127 * t);
                    b = Math.floor(128 + 127 * t);
                } else {
                    const t = -totalDisplacement / 4;
                    r = Math.floor(128 - 128 * t);
                    g = Math.floor(128 - 128 * t);
                    b = Math.floor(128 - 128 * t);
                }
            }
        }

        for (let dy = 0; dy < step && svgY + dy < height; dy++) {
            for (let dx = 0; dx < step && x + dx < width; dx++) {
                const index = ((svgY + dy) * width + (x + dx)) * 4;
                data[index] = r;
                data[index + 1] = g;
                data[index + 2] = b;
                data[index + 3] = 255;
            }
            }
        }
    }

    ctx.putImageData(imageData, 0, 0);
}

上面的代码是让 CPU 进行计算,你会发现绘制的阴影图明显有卡顿,一种解决方法是将程序中的 step 设置为大于 1 的值,例如设置为 4,就是每隔 4 个像素计算一次,但这样做相当于降低了图像的分辨率,在屏幕上的图像会出现马赛克。

治本的方法是将上述复杂运算和像素的绘制可以通过 WebGL 让显卡 GPU 完成,干涉图样瞬间就丝滑了。本程序默认优先使用 WebGL 渲染,现如今的显卡都应该能支持 WebGL 的硬件加速。除非在很古老的电脑上,才会回退到 Canvas2D 模式。

由于希望课件能单文件离线使用,因此不想引入 Three.js 之类的外部 3D 库,而且这种简单情况也无需使用这么重的库,所以用原生 WebGL 代码实现。有关原生 WebGL 的编程基础可参见本网站中《第 1 课 绘制一个三角形》及之后的几篇文章。

WebGL处理的是 3D 程序,要绘制 2D 程序的思路是用顶点着色器在屏幕上绘制一个覆盖全屏的矩形(由 2 个三角形组成),而绘制 2D 图像的核心代码在片段着色器中,你会发现原理与 Canvas2D 是完全相同的,只是语法有区别。片段着色器的代码如下:

precision highp float;

 uniform vec2 u_resolution;
 uniform float u_time;
 uniform float u_waveSpeed;
 uniform float u_frequency1;
 uniform float u_frequency2;
 uniform float u_amplitude1;
 uniform float u_amplitude2;
 uniform float u_phaseDiff;
 uniform vec2 u_source1;
 uniform vec2 u_source2;
 uniform int u_colorMode;

 void main() {
    // 转换WebGL坐标(原点在左下角)为SVG坐标(原点
    vec2 coord = vec2(gl_FragCoord.x, u_resolution.y - 1.0 -

    float wavelength1 = u_waveSpeed / u_frequency1;
    float wavelength2 = u_waveSpeed / u_frequency2;
    float k1 = 6.28318530718 / wavelength1;
    float k2 = 6.28318530718 / wavelength2;
    float omega1 = 6.28318530718 * u_frequency1;
    float omega2 = 6.28318530718 * u_frequency2;
    float phaseOffset1 = 0.0;
    float phaseOffset2 = u_phaseDiff * 3.14159265359;

    float r1 = distance(coord, u_source1);
    float r2 = distance(coord, u_source2);

    float phaseShift1 = phaseOffset1 * wavelength1 /
    float phaseShift2 = phaseOffset2 * wavelength2 /
    float maxDistance1 = u_waveSpeed * u_time + phaseShift1;
    float maxDistance2 = u_waveSpeed * u_time + phaseShift2;

    float displacement1 = 0.0;
    float displacement2 = 0.0;

    if (r1 <= maxDistance1) {
        float phase1 = omega1 * u_time - k1 * r1 + phaseOffset1;
        displacement1 = u_amplitude1 * sin(phase1);
    }

    if (r2 <= maxDistance2) {
        float phase2 = omega2 * u_time - k2 * r2 + phaseOffset2;
        displacement2 = u_amplitude2 * sin(phase2);
    }

    vec3 color;
    if (r1 > maxDistance1 && r2 > maxDistance2) {
        if (u_colorMode == 1) {
            color = vec3(0.0, 0.0, 0.0);
        } else {
            color = vec3(0.5, 0.5, 0.5);
        }
    } else {
        float totalDisplacement = displacement1 + displacement2;

        if (u_colorMode == 1) {
            // 高反差模式:蓝色波峰、橘色波谷、黑色平静
            if (totalDisplacement > 0.0) {
            float t = totalDisplacement / 4.0;
            color = vec3(0.369 * t, 0.639 * t, 1.0 * t);
        } else if (totalDisplacement < 0.0) {
            float t = -totalDisplacement / 4.0;
            color = vec3(1.0 * t, 0.647 * t, 0.0 * t);
        } else {
            color = vec3(0.0, 0.0, 0.0);
        }
        } else {
            // 灰度渐变模式
            if (totalDisplacement > 0.0) {
                float t = totalDisplacement / 4.0;
                color = vec3(0.5 + 0.498 * t, 0.5 + 0.498 * t, 0.5
            } else {
                float t = -totalDisplacement / 4.0;
                color = vec3(0.5 - 0.5 * t, 0.5 - 0.5 * t, 0.5 - 0.5 *
            }
        }
    }

    gl_FragColor = vec4(color, 1.0);
}

教学说明

在课本沪科版选择性必修一第二章 第四节 机械波的干涉和衍射中并没有明确提及机械波干涉的条件,出现的是这两句话:58 页的“……形成两列频率相同的水波……”,59 页的“两列振动情况相同的波源产生的波在某区域相遇时,……”。而在课本沪科版选择性必修二第四章 第三节 光的干涉第 81 页提及“振动方向相同、频率相等、相位差恒定的光源称为相干光源”。

我的看法是:对于机械波最好也明确说明干涉条件是两个波源的“振动方向相同、频率相等、相位差恒定”。在平时授课时,我只强调频率相等这个必要条件。

\[{\Delta \varphi = ({\omega _2} - {\omega _1})t - {\varphi _2} - {\varphi _1} - \frac{{2\pi ({r_2} - {r_1})}}{\lambda }}\]

当频率相等时,ω2ω1 = 0,要使Δφ恒定,还需要 φ2φ1= 0。即只有“频率相等”+“初相位差恒定”这两个条件同时满足,才能推导出“相位差恒定”。换句话说:频率相等是相位差恒定的必要条件,但不是充分条件,频率相等是相位差恒定的前提和基础。

用灰度图不容易看出图样的变化。为了观察非等幅情况下的干涉图样,此课件还添加了阴影图的“高反差(蓝色波峰/橘色波谷)”模式,现象更加明显。

通过滑动条让频率不同,或者拖动滑动条让相位差时刻改变,能看到振动加强、减弱的区域随之变化,某位置不再始终加强或始终减弱,干涉图样也不再稳定

相控阵雷达

在改变相位差时,从图中可以看出振动加强区域和减弱区域左右摇摆,这其实就是相控阵雷达最核心的原理——通过控制阵列中每个发射单元之间的相位差,来实现波束的扫描。

相控阵雷达(Phased Array Radar)又称电子扫描阵列(ESA)雷达,利用不同天线单元发射(接收)电磁波的相位差在空间合成高指向性、高增益、可转动的波束,从而实现对目标的搜索和跟踪。

相控阵雷达

一个简单的线性相控阵由 N 个相同的发射单元等距排列组成。

无相位差时(Δφ = 0

有相位差时(Δφ ≠ 0

\[\Delta \varphi = \frac{{2\pi }}{\lambda }d\sin \theta \]

其中 d 是单元间距,λ 是波长。

这样就通过电子方式控制 Δφ,就可以让波束在空间扫描,完全不需要转动天线

完整代码

 

发布时间:2026/3/7 22:36:59  阅读次数:268