6. WebGPU 将图像导入纹理

news2024/12/23 18:58:58

我们在上一篇文章中介绍了有关使用纹理的一些基础知识。在本文中,我们将介绍从图像导入纹理。

在上一篇文章中,通过调用 device.createTexture 创建了一个纹理,然后通过调用 device.queue.writeTexture 将数据放入纹理中。 device.queue 上还有另一个名为 device.queue.copyExternalImageToTexture 的函数,可以将图像复制到纹理中。

它可以采用 ImageBitmap ,所以让我们采用上一篇文章中的 magFilter 示例并将其更改为导入一些图像。

首先需要一些代码来从图像中获取 ImageBitmap

  async function loadImageBitmap(url) {
    const res = await fetch(url);
    const blob = await res.blob();
    return await createImageBitmap(blob, { colorSpaceConversion: 'none' });
  }

上面的代码传入图像的 url 并调用 fetch 。这将返回一个 Response 。然后使用它来加载一个不透明地表示图像文件数据的 Blob 。然后将它传递给 createImageBitmap ,这是一个标准的浏览器函数来创建一个 ImageBitmap 。我们通过 { colorSpaceConversion: ‘none’ } 告诉浏览器不要应用任何颜色空间。如果您希望浏览器应用颜色空间,则由您决定。通常在 WebGPU 中,可能会加载法线贴图或高度贴图或非颜色数据的图像。在这些情况下,我们绝对不希望浏览器弄乱图像中的数据。

现在我们有了创建 ImageBitmap 的代码,让我们加载并创建一个相同大小的纹理。

我们将加载这张图片
在这里插入图片描述

带有 F 的纹理是一个很好的示例纹理,因为我们可以立即看到它的方向。
在这里插入图片描述


  //const texture = device.createTexture({
  //  label: 'yellow F on red',
  //  size: [kTextureWidth, kTextureHeight],
  //  format: 'rgba8unorm',
  //  usage:
  //    GPUTextureUsage.TEXTURE_BINDING |
  //    GPUTextureUsage.COPY_DST,
  // });
  const url = 'resources/images/f-texture.png';
  const source = await loadImageBitmap(url);
  const texture = device.createTexture({
    label: url,
    format: 'rgba8unorm',
    size: [source.width, source.height],
    usage: GPUTextureUsage.TEXTURE_BINDING |
           GPUTextureUsage.COPY_DST |
           GPUTextureUsage.RENDER_ATTACHMENT,
  });

请注意, copyExternalImageToTexture 要求我们将 GPUTextureUsage.COPY_DST 和 GPUTextureUsage.RENDER_ATTACHMENT 标志包括在内。

那么我们可以将 ImageBitmap 复制到纹理

 // device.queue.writeTexture(
 //     { texture },
 //     textureData,
 //     { bytesPerRow: kTextureWidth * 4 },
 //     { width: kTextureWidth, height: kTextureHeight },
 // );
  device.queue.copyExternalImageToTexture(
    { source, flipY: true },
    { texture },
    { width: source.width, height: source.height },
  );

copyExternalImageToTexture 的参数是源、目标和大小。对于源,如果我们希望在加载时翻转纹理,我们可以指定 flipY: true 。

这样就可以了!

在这里插入图片描述

1. Generating mips on the GPU

在上一篇文章中,我们还生成了一个 mipmap,在那种方法下,可以轻松访问图像数据。导入图片时,我们可以将该图片绘制到二维画布中,调用 getImageData 获取数据,最后生成mips并上传。但那会很慢。它也可能是有损的,因为画布 2D 渲染的方式依赖于底层的实现方式。

当我们生成 mip 级别时,进行了双线性插值,这正是 GPU 使用 minFilter: linear 所做的。我们可以使用该功能在 GPU 上生成 mip 级别。

让我们修改上一篇文章中的 minmapFilter 示例,以使用 GPU 加载图像和生成 mip

首先,让我们更改创建纹理的代码以创建 mip 级别。我们需要知道要创建多少,可以这样计算

  const numMipLevels = (...sizes) => {
    const maxSize = Math.max(...sizes);
    return 1 + Math.log2(maxSize) | 0;
  };

我们可以用 1 个或多个数字调用它,它会返回所需的 mips 数,例如 numMipLevels(123, 456) 返回 9 。

  1. level 0: 123, 456
  2. level 1: 61, 228
  3. level 2: 30, 114
  4. level 3: 15, 57
  5. level 4: 7, 28
  6. level 5: 3, 14
  7. level 6: 1, 7
  8. level 7: 1, 3
  9. level 8: 1, 1

9 mip levels

Math.log2 tells us the power of 2 we need to make our number. In other words, Math.log2(8) = 3 because 23 = 8. Another way to say the same thing is, Math.log2 tells us how many times can we divide this number by 2.
Math.log2 告诉我们 2 的幂是我们需要的数字。换句话说, Math.log2(8) = 3 因为 2的3次方 = 8。另一种说法是, Math.log2 告诉我们可以将这个数字除以 2 多少次。

Math.log2(8)
          8 / 2 = 4
                  4 / 2 = 2
                          2 / 2 = 1

所以我们可以将 8 除以 2 三次。这正是我们需要计算要制作多少 mip 级别的内容。这是 Math.log2(largestSize) + 1 。 1 表示原始大小 mip 级别 0

所以,我们现在可以创建正确数量的 mip 级别

  const texture = device.createTexture({
    label: url,
    format: 'rgba8unorm',
    mipLevelCount: numMipLevels(source.width, source.height),
    size: [source.width, source.height],
    usage: GPUTextureUsage.TEXTURE_BINDING |
           GPUTextureUsage.COPY_DST |
           GPUTextureUsage.RENDER_ATTACHMENT,
  });
  device.queue.copyExternalImageToTexture(
    { source, flipY: true, },
    { texture },
    { width: source.width, height: source.height },
  );

为了生成下一个 mip 级别,我们将绘制一个带纹理的四边形,就像我们一直在做的那样,从现有的 mip 级别到下一个级别, minFilter: linear 。

Here’s the code 这是代码

  const generateMips = (() => {
    let pipeline;
    let sampler;
 
    return function generateMips(device, texture) {
      if (!pipeline) {
        const module = device.createShaderModule({
          label: 'textured quad shaders for mip level generation',
          code: `
            struct VSOutput {
              @builtin(position) position: vec4f,
              @location(0) texcoord: vec2f,
            };
 
            @vertex fn vs(
              @builtin(vertex_index) vertexIndex : u32
            ) -> VSOutput {
              var pos = array<vec2f, 6>(
 
                vec2f( 0.0,  0.0),  // center
                vec2f( 1.0,  0.0),  // right, center
                vec2f( 0.0,  1.0),  // center, top
 
                // 2st triangle
                vec2f( 0.0,  1.0),  // center, top
                vec2f( 1.0,  0.0),  // right, center
                vec2f( 1.0,  1.0),  // right, top
              );
 
              var vsOutput: VSOutput;
              let xy = pos[vertexIndex];
              vsOutput.position = vec4f(xy * 2.0 - 1.0, 0.0, 1.0);
              vsOutput.texcoord = vec2f(xy.x, 1.0 - xy.y);
              return vsOutput;
            }
 
            @group(0) @binding(0) var ourSampler: sampler;
            @group(0) @binding(1) var ourTexture: texture_2d<f32>;
 
            @fragment fn fs(fsInput: VSOutput) -> @location(0) vec4f {
              return textureSample(ourTexture, ourSampler, fsInput.texcoord);
            }
          `,
        });
        pipeline = device.createRenderPipeline({
          label: 'mip level generator pipeline',
          layout: 'auto',
          vertex: {
            module,
            entryPoint: 'vs',
          },
          fragment: {
            module,
            entryPoint: 'fs',
            targets: [{ format: texture.format }],
          },
        });
 
        sampler = device.createSampler({
          minFilter: 'linear',
        });
      }
 
      const encoder = device.createCommandEncoder({
        label: 'mip gen encoder',
      });
 
      let width = texture.width;
      let height = texture.height;
      let baseMipLevel = 0;
      while (width > 1 || height > 1) {
        width = Math.max(1, width / 2 | 0);
        height = Math.max(1, height / 2 | 0);
 
        const bindGroup = device.createBindGroup({
          layout: pipeline.getBindGroupLayout(0),
          entries: [
            { binding: 0, resource: sampler },
            { binding: 1, resource: texture.createView({baseMipLevel, mipLevelCount: 1}) },
          ],
        });
 
        ++baseMipLevel;
 
        const renderPassDescriptor = {
          label: 'our basic canvas renderPass',
          colorAttachments: [
            {
              view: texture.createView({baseMipLevel, mipLevelCount: 1}),
              clearValue: [0.3, 0.3, 0.3, 1],
              loadOp: 'clear',
              storeOp: 'store',
            },
          ],
        };
 
        const pass = encoder.beginRenderPass(renderPassDescriptor);
        pass.setPipeline(pipeline);
        pass.setBindGroup(0, bindGroup);
        pass.draw(6);  // call our vertex shader 6 times
        pass.end();
      }
 
      const commandBuffer = encoder.finish();
      device.queue.submit([commandBuffer]);
    };
  })();

上面的代码看起来很长,但它几乎与我们迄今为止在纹理示例中使用的代码完全相同。

有变化的地方如下:

  1. 使用了一个闭包来保留 3 个变量。 module 、 sampler 、 pipelineByFormat 。对于 module 和 sampler ,检查它们是否没有被设置,如果没有,就创建一个 GPUSShaderModule 和 GPUSampler ,这样可以保留并在将来使用。

  2. 有一对与所有示例几乎完全相同的着色器。唯一不同的是这部分

  //vsOutput.position = uni.matrix * vec4f(xy, 0.0, 1.0);
  //vsOutput.texcoord = xy * vec2f(1, 50);
  vsOutput.position = vec4f(xy * 2.0 - 1.0, 0.0, 1.0);
  vsOutput.texcoord = vec2f(xy.x, 1.0 - xy.y);

原先在着色器中的硬编码四边形位置数据从 0.0 到 1.0,因此,按照原样,只会覆盖正在绘制的右上角四分之一纹理,就像示例中一样。但现在需要它覆盖整个区域,因此通过乘以 2 并减去 1,我们得到一个从 -1,-1 到 +1,1 的四边形。

We also flip the Y texture coordinate. This is because when drawing to the texture +1, +1 is at the top right but we want the top right of the texture we are sampling to be there. The top right of the sampled texture is +1, 0
我们还翻转了 Y 纹理坐标。这是因为当绘制到纹理 +1 时,+1 位于右上角,但我们希望采样纹理的右上角在那里。采样纹理的右上角为+1, 0

  1. 对象pipelineByFormat ,用作将管线映射到纹理格式。这是因为管线需要知道要使用的格式。

  2. 我们检查是否已经有一个特定格式的管线,如果没有则创建一个

    if (!pipelineByFormat[texture.format]) {
      pipelineByFormat[texture.format] = device.createRenderPipeline({
        label: 'mip level generator pipeline',
        layout: 'auto',
        vertex: {
          module,
          entryPoint: 'vs',
        },
        fragment: {
          module,
          entryPoint: 'fs',
          targets: [{ format: texture.format }],
        },
      });
    }
    const pipeline = pipelineByFormat[texture.format];

这里唯一的主要区别是 targets 是从纹理的格式设置的,而不是我们在渲染到画布时使用的 presentationFormat

  1. 最后给 texture.createView设置一些参数

遍历每个 mip 级别。为最后一个 mip 创建一个绑定组,其中包含数据,设置 renderPassDescriptor 以绘制到下一个 mip 级别。然后为特定的 mip 级别编码一个 renderPass。当完成时。所有的 mip 都将被填满。

    let width = texture.width;
    let height = texture.height;
    let baseMipLevel = 0;
    while (width > 1 || height > 1) {
      width = Math.max(1, width / 2 | 0);
      height = Math.max(1, height / 2 | 0);
 
      const bindGroup = device.createBindGroup({
        layout: pipeline.getBindGroupLayout(0),
        entries: [
          { binding: 0, resource: sampler },
          { binding: 1, resource: texture.createView({baseMipLevel, mipLevelCount: 1}) }, //here
        ],
      });
 
      ++baseMipLevel; //here
 
      const renderPassDescriptor = {
        label: 'our basic canvas renderPass',
        colorAttachments: [
          {
            view: texture.createView({baseMipLevel, mipLevelCount: 1}), //here
            loadOp: 'clear',
            storeOp: 'store',
          },
        ],
      };
 
      const pass = encoder.beginRenderPass(renderPassDescriptor);
      pass.setPipeline(pipeline);
      pass.setBindGroup(0, bindGroup);
      pass.draw(6);  // call our vertex shader 6 times
      pass.end();
    }
 
    const commandBuffer = encoder.finish();
    device.queue.submit([commandBuffer]);

让我们创建一些工具函数,使将图像加载到纹理中并生成 mips 变得简单

这是一个更新第一个 mip 级别并可选择翻转图像的函数。如果图像具有 mip 级别,那么将生成它们。

  function copySourceToTexture(device, texture, source, {flipY} = {}) {
    device.queue.copyExternalImageToTexture(
      { source, flipY, },
      { texture },
      { width: source.width, height: source.height },
    );
 
    if (texture.mipLevelCount > 1) {
      generateMips(device, texture);
    }
  }

这是另一个函数,给定源(在本例中为 ImageBitmap )将创建匹配大小的纹理,然后调用前一个函数用数据填充它

  function createTextureFromSource(device, source, options = {}) {
    const texture = device.createTexture({
      format: 'rgba8unorm',
      mipLevelCount: options.mips ? numMipLevels(source.width, source.height) : 1,
      size: [source.width, source.height],
      usage: GPUTextureUsage.TEXTURE_BINDING |
             GPUTextureUsage.COPY_DST |
             GPUTextureUsage.RENDER_ATTACHMENT,
    });
    copySourceToTexture(device, texture, source, options);
    return texture;
  }

这是一个给定 url 的函数,它将加载 url 作为 ImageBitmap 调用调用前面的函数来创建纹理并用图像的内容填充它。

  async function createTextureFromImage(device, url, options) {
    const imgBitmap = await loadImageBitmap(url);
    return createTextureFromSource(device, imgBitmap, options);
  }

通过这些设置,mipmapFilter 示例的唯一主要更改是

//  const textures = [
//    createTextureWithMips(createBlendedMipmap(), 'blended'),
//    createTextureWithMips(createCheckedMipmap(), 'checker'),
//  ];
  const textures = await Promise.all([
    await createTextureFromImage(device,
        'resources/images/f-texture.png', {mips: true, flipY: false}),
    await createTextureFromImage(device,
        'resources/images/coins.jpg', {mips: true}),
    await createTextureFromImage(device,
        'resources/images/Granite_paving_tileable_512x512.jpeg', {mips: true}),
  ]);

上面的代码从上方加载 F 纹理以及这 2 个平铺纹理

在这里插入图片描述

效果图如下:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2. Import Canvas

copyExternalImageToTexture 还有其他来源。另一个是 HTMLCanvasElement 。可以使用它在 2d 画布中绘制东西,然后在 WebGPU 中的纹理中获得结果。当然,您可以使用 WebGPU 绘制纹理,并将您刚刚绘制的纹理也用于您渲染的其他内容。事实上,我们只是这样做了,渲染到一个 mip 级别,然后使用该 mip 级别的纹理附件来渲染到下一个 mip 级别。

但是,有时使用 2d 画布可以使某些事情变得容易。 2d 画布具有相对高级的 API。

所以,首先让我们制作某种canvas 动画。

const size = 256;
const half = size / 2;
 
const ctx = document.createElement('canvas').getContext('2d');
ctx.canvas.width = size;
ctx.canvas.height = size;
 
const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`;
 
function update2DCanvas(time) {
  time *= 0.0001;
  ctx.clearRect(0, 0, size, size);
  ctx.save();
  ctx.translate(half, half);
  const num = 20;
  for (let i = 0; i < num; ++i) {
    ctx.fillStyle = hsl(i / num * 0.2 + time * 0.1, 1, i % 2 * 0.5);
    ctx.fillRect(-half, -half, size, size);
    ctx.rotate(time * 0.5);
    ctx.scale(0.85, 0.85);
    ctx.translate(size / 16, 0);
  }
  ctx.restore();
}
 
function render(time) {
  update2DCanvas(time);
  requestAnimationFrame(render);
}
requestAnimationFrame(render);

在这里插入图片描述
在这里插入图片描述

要将该画布导入 WebGPU,只需对之前的示例进行一些更改。

需要创建一个大小合适的纹理。最简单的方法就是使用上面写的相同代码

  const texture = createTextureFromSource(device, ctx.canvas, {mips: true});
 
  const textures = await Promise.all([
    //await createTextureFromImage(device,
    //    'resources/images/f-texture.png', {mips: true, flipY: false}),
    //await createTextureFromImage(device,
    //    'resources/images/coins.jpg', {mips: true}),
    //await createTextureFromImage(device,
    //    'resources/images/Granite_paving_tileable_512x512.jpeg', {mips: true}),
    texture,
  ]);

然后我们需要切换到 requestAnimationFrame 循环,更新2D画布,然后上传到WebGPU

  // function render() {
  function render(time) {
    update2DCanvas(time); //here
    copySourceToTexture(device, texture, ctx.canvas); //here
 
     ...
 
 
    requestAnimationFrame(render);
  }
  requestAnimationFrame(render);
 
  const observer = new ResizeObserver(entries => {
    for (const entry of entries) {
      const canvas = entry.target;
      const width = entry.contentBoxSize[0].inlineSize;
      const height = entry.contentBoxSize[0].blockSize;
      canvas.width = Math.min(width, device.limits.maxTextureDimension2D);
      canvas.height = Math.min(height, device.limits.maxTextureDimension2D);
     // render();
    }
  });
  observer.observe(canvas);
 
  canvas.addEventListener('click', () => {
    texNdx = (texNdx + 1) % textures.length;
   // render();
  });

With that we’re able to upload a canvas AND generate mips levels for it
这样我们就可以上传画布并为其生成 mips 级别

在这里插入图片描述

3. Importing Video 导入视频

以这种方式导入视频也没什么不同。可以创建一个 元素并将其传递给在上一个示例中将画布传递给的相同函数,它应该只需要稍作调整即可

11

在这里插入图片描述

ImageBitmap 和 HTMLCanvasElement 的宽度和高度与 width 和 height 属性相同,但 HTMLVideoElement 的宽度和高度在 videoWidth 和 videoHeight 上。所以,让我们更新代码来处理这个差异

  function getSourceSize(source) {   //here
    return [
      source.videoWidth || source.width,
      source.videoHeight || source.height,
    ];
  }
 
  function copySourceToTexture(device, texture, source, {flipY} = {}) {
    device.queue.copyExternalImageToTexture(
      { source, flipY, },
      { texture },
     // { width: source.width, height: source.height },
      getSourceSize(source), //here
    );
 
    if (texture.mipLevelCount > 1) {
      generateMips(device, texture);
    }
  }
 
  function createTextureFromSource(device, source, options = {}) {
    const size = getSourceSize(source); //here
    const texture = device.createTexture({
      format: 'rgba8unorm',
      //mipLevelCount: options.mips ? numMipLevels(source.width, source.height) : 1,
      //size: [source.width, source.height],
      mipLevelCount: options.mips ? numMipLevels(...size) : 1,
      size, //here
      usage: GPUTextureUsage.TEXTURE_BINDING |
             GPUTextureUsage.COPY_DST |
             GPUTextureUsage.RENDER_ATTACHMENT,
    });
    copySourceToTexture(device, texture, source, options);
    return texture;
  }

那么,让我们设置一个视频元素

  const video = document.createElement('video');
  video.muted = true;
  video.loop = true;
  video.preload = 'auto';
  video.src = 'resources/videos/Golden_retriever_swimming_the_doggy_paddle-360-no-audio.webm';
 
  const texture = createTextureFromSource(device, video, {mips: true});

并在渲染时更新它

  //function render(time) {
  //  update2DCanvas(time);
  //  copySourceToTexture(device, texture, ctx.canvas);
  function render() {
    copySourceToTexture(device, texture, video);

视频的一个复杂问题是我们需要等待它们开始播放,然后再将它们传递给 WebGPU。在现代浏览器中,我们可以通过调用 video.requestVideoFrameCallback 来做到这一点。每次有新帧可用时它都会调用我们,因此可以使用它来确定何时至少有一个帧可用。

对于备用方案,我们可以等待时间提前并祈祷🙏因为可悲的是,旧的浏览器很难知道什么时候可以安全地使用视频😀

  function startPlayingAndWaitForVideo(video) {
    return new Promise((resolve, reject) => {
      video.addEventListener('error', reject);
      if ('requestVideoFrameCallback' in video) {
        video.requestVideoFrameCallback(resolve);
      } else {
        const timeWatcher = () => {
          if (video.currentTime > 0) {
            resolve();
          } else {
            requestAnimationFrame(timeWatcher);
          }
        };
        timeWatcher();
      }
      video.play().catch(reject);
    });
  }
 
  const video = document.createElement('video');
  video.muted = true;
  video.loop = true;
  video.preload = 'auto';
  video.src = 'resources/videos/Golden_retriever_swimming_the_doggy_paddle-360-no-audio.webm';
  await startPlayingAndWaitForVideo(video);
 
  const texture = createTextureFromSource(device, video, {mips: true});

另一个复杂的问题是我们需要等待用户与页面交互,然后才能开始播放视频 [1] 。让我们添加一些带有播放按钮的 HTML。

  <body>
    <canvas></canvas>
    <div id="start">
      <div>▶️</div>
    </div>
  </body> 

还有一些 CSS 让它居中

#start {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
}
#start>div {
  font-size: 200px;
  cursor: pointer;
}

然后写一个函数等待它被点击并隐藏它。

  function waitForClick() {
    return new Promise(resolve => {
      window.addEventListener(
        'click',
        () => {
          document.querySelector('#start').style.display = 'none';
          resolve();
        },
        { once: true });
    });
  }
 
  const video = document.createElement('video');
  video.muted = true;
  video.loop = true;
  video.preload = 'auto';
  video.src = 'resources/videos/Golden_retriever_swimming_the_doggy_paddle-360-no-audio.webm';
  await waitForClick();
  await startPlayingAndWaitForVideo(video);
 
  const texture = createTextureFromSource(device, video, {mips: true});

这样我们就应该得到有纹理的视频

在这里插入图片描述

可以进行的一项优化。只能在视频发生变化时更新纹理。

例如

  const video = document.createElement('video');
  video.muted = true;
  video.loop = true;
  video.preload = 'auto';
  video.src = 'resources/videos/Golden_retriever_swimming_the_doggy_paddle-360-no-audio.webm';
  await waitForClick();
  await startPlayingAndWaitForVideo(video);
 
  let alwaysUpdateVideo = !('requestVideoFrameCallback' in video);
  let haveNewVideoFrame = false;
  if (!alwaysUpdateVideo) {
    function recordHaveNewFrame() {
      haveNewVideoFrame = true;
      video.requestVideoFrameCallback(recordHaveNewFrame);
    }
    video.requestVideoFrameCallback(recordHaveNewFrame);
  }
 
  ...
 
  function render() {
    if (alwaysUpdateVideo || haveNewVideoFrame) {
      haveNewVideoFrame = false;
      copySourceToTexture(device, texture, video);
    }
 
    ...

通过此更改,我们只会为每个新帧更新视频。因此,例如,在显示速率为每秒 120 帧的设备上,我们将以每秒 120 帧的速度绘制,这样动画、相机移动等就会很流畅。但是,纹理只会以其自己的帧速率(例如 30fps)更新。

但! WebGPU 对高效使用视频有特别的支持

我们将在另一篇文章中介绍。上面的方式,使用 device.query.copyExternalImageToTexture 其实就是在复制。制作副本需要时间。例如,4k 视频的分辨率通常为 3840 × 2160,对于 rgba8unorm 来说,每帧需要复制 31meg 的数据。外部纹理让您可以直接使用视频数据(无副本),但需要不同的方法并有一些限制。

4. 注释1

有多种方法可以让视频(通常没有音频)自动播放,而无需等待用户与页面交互。它们似乎会随着时间而改变,因此我们不会在这里讨论解决方案。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/638508.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Axure教程—穿梭框(中继器+动态面板)

本文将教大家如何用AXURE中动态面板和中继器制作穿梭框效果 一、效果 预览地址&#xff1a;https://8k99mh.axshare.com 下载地址&#xff1a;https://download.csdn.net/download/weixin_43516258/87897661?spm1001.2014.3001.5503 二、功能 在待选区域选项中可以选择一个选…

CURL获取与使用

背景&#xff1a;在日常工作中&#xff0c;经常会遇到需要获取CURL构造请求来进行问题定位&#xff0c;那如何获取及使用CURL则成为一个测试人员必备的技能&#xff1b; CURL是什么 CURL是一个命令行工具&#xff0c;开发人员使用它来与服务器进行数据交互。 如何获取完整 C…

Python开源自动化工具Playwright安装及介绍

目录 前言 1、Playwright介绍 2、Playwright安装 3、实操演示 4、小结 总结&#xff1a; 前言 微软开源了一个非常强大的自动化项目叫 playwright-python 它支持主流的浏览器&#xff0c;包含&#xff1a;Chrome、Firefox、Safari、Microsoft Edge 等&#xff0c;同时支…

简单使用Hystrix

使用Hystrix之前&#xff0c;需要先对SpringCloud有所了解&#xff0c;然后才会使用的顺畅&#xff0c;它是我们SpringCould的一种保护机制&#xff0c;非常好用。 下面直接开始 先导入Hystrix所需要的依赖 <!-- 引入openfiegn--> <dependency> <groupId>org…

Java学习笔记(视频:韩顺平老师)3.0

如果你喜欢这篇文章的话&#xff0c;请给作者点赞哟&#xff0c;你的支持是我不断前进的动力。 因为作者能力水平有限&#xff0c;欢迎各位大佬指导。 目录 如果你喜欢这篇文章的话&#xff0c;请给作者点赞哟&#xff0c;你的支持是我不断前进的动力。 算数运算符 号使用…

体验 TDengine 3.0 高性能的第一步,请学会控制建表策略

正如我们之前所言&#xff0c;在 3.0 当中&#xff0c;我们在产品底层做了很大的变化调整&#xff0c;除了架构更加科学高效以外&#xff0c;用户体验也是我们重点优化的方向。以之前一篇文章为例&#xff1a;对于 Update 功能&#xff0c;用户不再需要任何配置 &#xff0c;默…

社交泛娱乐出海如何抓住AIGC?我在融云WICC上看到了答案

大模型掀起的AIGC时代&#xff0c;所有企业的所有业务与产品都值得利用大模型技术重做一遍&#xff0c;接下来也将有越来越多依托AIGC技术的创新应用涌现。关注【融云全球互联网通信云】了解更多 在社交泛娱乐赛道&#xff0c;AI大模型技术也呈现出了加速落地的态势。日前&…

功能测试如何转型自动化测试

在互联网行业&#xff0c;我们是那些被遗忘的技术人。 很多人都觉得&#xff0c;传统开发、运维才是技术含量的一个工作。 但是测试的入门门槛比较低&#xff0c;所做的事情相对有限&#xff0c; 这是我之前跟一些大型互联网软件测试负责人大牛们聊天的时候发现&#xff0c;…

学网络安全常用的10大工具

从事网络安全工作&#xff0c;手上自然离不开一些重要的网络安全工具。今天&#xff0c;分享10大网络安全工具。 一、Kali Linux Kali 是一个基于 Debian 的 Linux 发行版。它的目标就是为了简单&#xff1a;在一个实用的工具包里尽可能多的包含渗透和审计工具。Kali 实现了这…

【JMeter】 二次开发插件开发 Dubbo 接口测试插件浅析

概述 在一些企业中&#xff0c;各类业务系统非常丰富&#xff0c;相互之间或对外提供很多的服务或接口 这些服务或接口中&#xff0c;有很多是需要强契约约束的&#xff0c;服务的提供方、服务的使用方必须遵守相同契约 这类服务最典型的就是RPC&#xff0c;其中应用广泛的有Du…

一文读懂,WAF阻止恶意攻击的8种方法

WAF&#xff08;Web 应用程序防火墙&#xff09;是应用程序和互联网流量之间的第一道防线&#xff0c;它监视和过滤 Internet 流量以阻止不良流量和恶意请求&#xff0c;WAF 是确保 Web 服务的可用性和完整性的重要安全解决方案。 它通过充当保护 Web 应用程序服务器免受恶意客…

属性和方法

类的属性 变量&#xff1a;1&#xff0c;按照数据类型来分基本数据类型&#xff0c;引用数据类型 2&#xff0c;按照变量在类中声明的位置不同&#xff1a;成员变量&#xff08;属性&#xff09;、局部属性&#xff08;方法内&#xff0c;构造器内&#xff0c;代码块内等&…

android 如何分析应用的内存(六)

android 如何分析应用的内存&#xff08;六&#xff09; 接上文&#xff0c;本系列文章&#xff0c;最重要的部分——————对native堆内存的分析&#xff0c;即将上演 分成六大板块&#xff1a; 手动实现&#xff0c;new和delete&#xff0c;以及malloc和freee&#xff0…

LVS+Keepalived负载均衡高可用群集(往事清零,万事顺意)

一、Keepalived高可用详解 1.应用场景 在企业应用中&#xff0c;单台服务器承担应用存在单点故障的危险。单点故障一旦发生&#xff0c;企业服务将发生中断&#xff0c;造成极大的危害。所以需要群集实现高可用性&#xff0c;保证服务稳定。 2.介绍和原理简介 Keepalived是…

【JAVA开发环境配置】 卸载JDK很简单, 一分钟帮你搞定!

&#x1f680; 个人主页 极客小俊 ✍&#x1f3fb; 作者简介&#xff1a;web开发者、设计师、技术分享博主 &#x1f40b; 希望大家多多支持一下, 我们一起进步&#xff01;&#x1f604; &#x1f3c5; 如果文章对你有帮助的话&#xff0c;欢迎评论 &#x1f4ac;点赞&#x1…

中科三方:DNS云解析技术浅析

智能DNS云解析通过其智能解析&#xff0c;健康监测&#xff0c;负载均衡&#xff0c;宕机切换等高可用性的功能特性&#xff0c;给客户带来快捷&#xff0c;安全&#xff0c;流畅的上网体验。传统的DNS因为其解析时间冗长&#xff0c;易被劫持&#xff0c;无法精准调配用户的流…

【jupyter notebook】Anaconda prompt查询版本包(已安装的包列表、可以安装的包列表)

目录 0.环境介绍 1.查询当前已安装的某包信息 2.查询某包的所有版本 3.查看已安装的各个包的版本 0.环境介绍 windows Anaconda 1.查询当前已安装的某包信息 信息包含包名和版本&#xff0c;以包【matplotlib】为例 conda list matplotlib 2.查询某包的所有版本 conda s…

playwright-自动化测试

这里写目录标题 安装运行记录操作执行脚本 安装 &#xff08;1&#xff09;安装Playwright依赖库&#xff08;Playwright支持Async\Await语法&#xff0c;故需要Python3.7&#xff09; pip install playwright &#xff08;2&#xff09;安装Chromium、Firefox、WebKit等浏览…

【Unity Shader】平面投影实现阴影

介绍 球体和立方体挂载下面这个shader&#xff0c;就是多渲染一个阴影投影到y0的平面上 // shader&#xff0c;放在需要显示阴影的对象上 Shader "Custom/PlanarShadow1" {Properties{_Instensity("Shininess", Range(2, 4)) 2.0 //光照强度_Diffuse(&…

Mac下自定义快捷键

1、系统偏好设置-键盘-快捷键-App快捷键 2、确定要对什么进行自定义 比如 我要对DBeaver的运行自定义&#xff0c;我个人喜欢用cmd r 3、添加App快捷键 注意&#xff0c;菜单标题就是应用里的&#xff0c;必须要一模一样&#xff0c;哪怕是空格都不能少。也不能多。 4、对…