7. WebGPU 将图像导入纹理

news2024/11/24 16:08:18

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

在上一篇文章中,通过调用 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 告诉我们 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/650438.html

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

相关文章

从小白到大神之路之学习运维第41天---第三阶段---Redis高可用集群(redis 的主从复制、redis的哨兵模式操作)

第三阶段基础 时 间&#xff1a;2023年6月15日 参加人&#xff1a;全班人员 内 容&#xff1a; Redis高可用集群 目录 一、redis主从复制原理介绍 主从复制特点&#xff1a; 主从复制实现原理&#xff1a; 二、主从复制实现操作&#xff08;多机实例实现&#xff09…

Mysql 表的七种联接方式【附带练习sql】

联接 七种JOIN介绍 图形联接方式说明SQL内联接共有部分SELECT <select_list> FROM TableA A INNER JOIN TableB B ON A.Key B.Key;左联接A表独有共有部分SELECT <select_list> FROM TableA A LEFT JOIN TableB B ON A.Key B.Key;右联接B表独有共有部分SELECT &…

linux spi相关调试

在Linux系统中&#xff0c;SPI 的用户模式设备接口的驱动源码位于 drivers/spi/spidev.c&#xff0c;在应用层生成 /dev/spidev* 的节点&#xff0c;可以通过 read、 write 达到与硬件设备的 SPI 通信。下面介绍spidev驱动移植和应用程序编写方法。 SPI &#xff08;serial pe…

阿里 P8 面试官总结的《2023 最新 java 面试题》限时开源了

国内的互联网面试&#xff0c;恐怕是现存的、最接近科举考试的制度。 以美国为例&#xff0c;北美工程师面试比较重视算法&#xff08;Coding&#xff09;&#xff0c;近几年也会加入 Design 轮&#xff08;系统设计和面向对象设计 OOD&#xff09;和 BQ 轮&#xff08;Behavi…

怎么学习数据库的相关知识? - 易智编译EaseEditing

学习数据库的相关知识可以按照以下步骤进行&#xff1a; 确定学习目标&#xff1a; 了解数据库的基本概念、原理和常用术语&#xff0c;学习数据库管理系统&#xff08;DBMS&#xff09;的功能和特性&#xff0c;以及掌握数据库设计和查询语言等方面的知识。 学习数据库基础知…

【闭包函数与装饰器大全】——python基础

目录索引 闭包&#xff1a;闭包三要素&#xff1a;闭包的作用&#xff1a;闭包演示&#xff1a;闭包的意义&#xff1a; 装饰器&#xff1a;特点&#xff1a;实例演示&#xff1a;实例演示2之参数&#xff1a; 装饰器常用的场景&#xff1a;编写一个计时的装饰器&#xff1a;*普…

AIGC和虚拟现实为什么必然产物

背景 在流量存量时代&#xff0c;内容运营重要性不言而喻。在流量时代&#xff0c;内容可以不要过于多样化和差异化&#xff0c;只需要有足够多的人流量&#xff0c;按流量转化比率来看&#xff0c;1000个人有1%概率转化&#xff0c;素材不变只要增加足够多的流量那就一定会有…

永久存储:文件处理与路径处理

&#x1f4e2;博客主页&#xff1a;盾山狂热粉的博客_CSDN博客-C、C语言,机器视觉领域博主&#x1f4e2;努力努力再努力嗷~~~✨ &#x1f4a1;大纲 ⭕如何将数据永久的存放到硬盘上 &#x1f449;不要打开文件&#xff0c;然后直接关闭文件&#xff0c;会导致截断 一、如何操作…

OneNote支持Markdown:高亮代码

文章目录 结论在前效果安装日常工作流&#xff08;高亮代码块&#xff09; 结论在前 markdown对我来说是伪需求&#xff0c;真正需要的是高亮代码&#xff0c;通过Github/NoteHighlight2016这个开源插件可实现这个插件虽然叫2016&#xff0c;但实测onenote 2013也是可以用的 效…

华为OD机试之数组去重和排序(Java源码)

数组去重和排序 题目描述 给定一个乱序的数组&#xff0c;删除所有的重复元素&#xff0c;使得每个元素只出现一次&#xff0c;并且按照出现的次数从高到低进行排序&#xff0c;相同出现次数按照第一次出现顺序进行先后排序。 输入描述 一个数组 输出描述 去重排序后的数组…

使用微信怎么图片投票怎么设置qq里面投票是如何

手机互联网给所有人都带来不同程度的便利&#xff0c;而微信已经成为国民的系统级别的应用。 现在很多人都会在微信群或朋友圈里转发投票&#xff0c;对于运营及推广来说找一个合适的投票小程序能够提高工作效率&#xff0c;提高活动的影响力。 那么微信做投票的小程序哪个比较…

苹果头显Vision Pro深度解读2 软件开发者谁最受益?

1 软件开发者谁最受益&#xff1f; 开发Vision pro这种设备的软件啊&#xff0c;首先得跟vision pro本身的特点结合起来&#xff0c;比如它本质上并不是一个很适合移动的的设备。用户大多数时候&#xff0c;都是坐着&#xff0c;站着&#xff0c;躺着使用。 所以典型的应用场…

【Linux】文件描述符与重定向操作

系列文章 收录于【Linux】文件系统 专栏 对于Linux下文件的写入与读取&#xff0c;以及文件原理还有疑惑的可以看看上一篇文章浅谈文件原理与操作。 目录 系列文章 再谈文件描述符 ​编辑 IO函数的本质 一切皆文件 文件重定向 原理 系统接口 再谈文件描述符 &#x…

C语言 指针(特别篇)

本篇目录 C语言 指针&#xff08;特别篇&#xff09;内存地址简要介绍C语言指针C语言的指针可以指向什么?取地址符 &&#xff08;Address-of Operator&#xff09;C语言中的 * 号运算符示例集&#xff1a;指向变量的指针指向数组的指针指向字符串的指针二级指针指针数组的…

doris docker部署 1.2.4.1版本

目录 写在前面 镜像编译 准备工作 下载编译好的包 fe be 编辑初始化文件 fe be 编辑Dockerfile fe be 构建镜像 fe be 构建结果 镜像运行 fe be 修改配置 添加udf依赖 启动be 注册be 错误分析 写在前面 以下操作语句按顺序执行即可&#xff0c;如果需要…

泛型Generic

泛型 1. 介绍1.1 使用泛型的好处 2. 自定义泛型结构&#xff1a;泛型类、泛型接口&#xff1b;泛型方法2.1 泛型类、泛型接口2.2 泛型方法 3. 其他3.1 泛型在继承方面的体现3.2 通配符使用3.2.1 有限制的通配符 1. 介绍 把元素的类型设计成一个参数&#xff0c;这个类型参数叫…

数组6大排序算法

快速排序 核心算法&#xff1a; 1.取一个基准值&#xff08;一般是数组中间的元素&#xff09;&#xff0c;遍历数组&#xff0c;比基准值大的放右边&#xff0c;小的放左边&#xff0c;相等的则不动 2.分别创建三个数组来存储元素&#xff0c;最后将三个数组拼接起来 3.循…

Java替换Jar文件中的class文件方法

备份源文件 文件不重要的话可以不需要备份&#xff0c;线上环境务必备份方便回滚 mkdir bak cp test.jar bak 查看class文件所在目录 jar -tvf test.jar | grep Time.class 标红内容就是需要替换的class文件&#xff0c;如果有多个文件需要替换依次执行2&#xff0c;3步骤…

webSocket实时通信02——基于Spring【纯后端——JAVA】

这里是基于Spring整合websoket后来实现的实时通信&#xff0c;这里只有java的代码&#xff0c;通过在线网站 http://www.websocket-test.com/测试即可 1. 导包 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-star…

网络安全合规-职业发展路线

网络安全人才一将难求&#xff0c;缺口高达 95% 在以前&#xff0c;很多政企单位在进行 IT 部门及岗位划分时&#xff0c;只有研发和运维部门&#xff0c;安全人员直接归属到基础运维部&#xff1b;而现在&#xff0c;越来越多单位为了满足国家安全法律法规的要求&#xff0c;…