Three.js 模型体素化原理及实现

news2024/11/30 7:36:23

在本文中,我们探索了 3D 模型的体素化过程,重点是使用导入的 glTF 模型创建 3D 像素艺术。 本文包括一个最终演示,涵盖了可以使用体素化实现的各种 3D 效果。 我们将提供涵盖以下主题的分步指南:

  • 确定 XYZ 坐标是否在 3D 网格内的数学方法
  • 将标准 Three.js 几何体分解为体素
  • 导入的 glTF 模型的实现
  • 体素化方法的局限性和优化
  • 设置体素样式和动画的方法

在这里插入图片描述

推荐:用 NSDT设计器 快速搭建可编程3D场景。

1、体素表示

体素表示的概念涉及跟踪 3D 网格并使用称为体素(Voxel)的基本元素填充其体积。 虽然体素是“体积像素”的缩写,但通常被渲染为 3D 框,而任何 3D 形状都可以用作艺术品中的体素。

在我们使用体素样式之前,我们需要知道如何将形状分解为体素,即如何在网格内定位一组 XYZ 坐标。

2、在网格内定位坐标

有一个优雅的技巧可以确定一个点是否在封闭的 3D 网格内。 当其表面没有孔或间隙时,网格被认为是“封闭的”。 不要将此与网格本身的复杂性混淆,只要其表面是连续的,网格可能有各种孔洞。

在这里插入图片描述

左:开放网格; 右:闭合网格。
让我们测试一个随机的 XYZ 坐标(蓝色框)是否在甜甜圈形状内。 为此,我们从 XYZ 坐标投射一条射线,计算射线与网格表面相交的次数,并检查次数是否为奇数。

在这里插入图片描述

如果框在网格内部,则交点数为奇数

为什么奇怪? 想象沿着射线从远处移动到 XYZ 点。 我们从网格外开始。 在第一个交点之后,我们进入网格体; 经过第二个路口,我们退出; 经过第三个路口,我们再次进入; 等等。 如果交点的数量是偶数(包括零),则该点在网格之外。 如果交叉点的数量是奇数,则该点在网格内部。 就如此容易!

实际上,我们计算从 XYZ 到无穷大的交点。 通过倒数更容易解释这个技巧,但无论计数方向如何,交叉点的数量都是相同的。 通常,光线的方向也无关紧要。 如果网格是封闭的,我们可以将射线从 XYZ 投射到任何地方并得到正确的结果。 唯一的例外是光线平行于网格表面投射时; 在这种情况下,我们将无法注册交点。

3、Three.js 中的实现

我们首先创建一个基本的 3D 场景,其中包含一些灯光、接收阴影的地板平面、渐变背景、轨道控件和用于场景设置的 GUI 模块。 下面场景中唯一不符合标准的是包含灯光和底部面板的 lightHolder 组。 通常,我们使用 OrbitControls 围绕静态场景旋转相机。 要创建旋转物体和静态相机的错觉,我们可以在 OrbitControls 更新时让 lightHolder 与相机一起旋转。 这是对 Three.js 样板的一个很好的补充,但与体素化无关。

接下来,我们添加要分解为体素的网格。 它可以是 Sphere、Torus、Torus Knot 或任何其他闭合网格(例如,LatheGeometry 是开放的,需要更多的工作来处理)。

首先,我们需要可以在网格内部的 XYZ 坐标。 为了获得它们,我们使用预定义的网格步穿越网格边界框。 对于像素式体素,我们使用简单的方形网格,但坐标采样可以是随机函数或更高级的东西。

function voxelizeMesh(mesh) {
    const boundingBox = new THREE.Box3().setFromObject(mesh);

    for (let i = box.min.x; i < box.max.x; i += params.gridSize) {
        for (let j = box.min.y; j < box.max.y; j += params.gridSize) {
            for (let k = box.min.z; k < box.max.z; k += params.gridSize) {
                const pos = new THREE.Vector3(i, j, k);
                if (isInsideMesh(pos, mesh)) {
                    voxels.push({
                        position: pos
                    })
                }
            }
        }
    }
}

一旦我们有了可能位于网格内部的候选坐标列表,我们就可以使用 THREE.Raycaster 从每个坐标向负 Y 方向投射一条射线,并计算交叉点的数量。 如果交叉点的数量是奇数,我们将坐标保存到体素数组中。

function isInsideMesh(pos, mesh) {
    rayCaster.set(pos, {x: 0, y: -1, z: 0});
    rayCasterIntersects = rayCaster.intersectObject(mesh, false);
    // we need odd number of intersections
    return rayCasterIntersects.length % 2 === 1; 
}

重要说明:默认情况下,任何 Three.js 材质都设置为仅渲染网格的正面。 这不仅会影响网格的外观,还会影响光线投射器的行为。 当材质的侧面属性设置为 THREE.Front 时,光线投射器只能检测与从外部观察的网格表面的交点。 为了从网格内部和外部检测交叉点,我们需要将材质的边属性更改为 THREE.DoubleSide,即使原始网格不会被渲染。

const outerShapeGeometry = geometries[params.geometry];
const outerShapeMaterial = new THREE.MeshLambertMaterial({
    color: 0xffff55,
    side: THREE.DoubleSide
});
outerShapeMesh = new THREE.Mesh(outerShapeGeometry, outerShapeMaterial);

一旦我们在体素数组中有了 XYZ 位置,就可以使用它们来放置体素。

要绘制体素,需要大量具有相同几何形状和材料但位置不同的对象。 这是 Three.js 实例化网格(Instanced Mesh)的完美用例。 使用实例化网格,可以实现出色的性能,同时仍然能够为每个体素设置颜色和变换。

我们可以创建一个 THREE.InstancedMesh,其实例数等于 voxels.length。 请记住以下几点:

  • 为了获得漂亮的像素化外观,使用了 RoundedBoxGeometry。 但是,它不包含在基本的 Three.js 构建中,必须手动导入。
  • 体素颜色现在可以简单地设置为材质的属性,但稍后将对颜色做更多的工作。
  • 让所有体素投射和接收阴影对于性能来说是相当昂贵的,但对于这个项目中的体素数量来说仍然是可以接受的。
import {RoundedBoxGeometry} from 'three/addons/geometries/RoundedBoxGeometry.js';

voxelGeometry = new RoundedBoxGeometry(params.boxSize, params.boxSize, params.boxSize, 2, params.boxRoundness);
voxelMaterial = new THREE.MeshLambertMaterial({
    color: new THREE.Color(0xffff55)
});
instancedMesh = new THREE.InstancedMesh(voxelGeometry, voxelMaterial, voxels.length);
instancedMesh.castShadow = true;
instancedMesh.receiveShadow = true;
scene.add(instancedMesh);

将体素数组的 XYZ 位置设置为实例的标准方法涉及使用哑(dummy)对象:

dummy = new THREE.Object3D();

for (let i = 0; i < voxels.length; i++) {
    dummy.position.copy(voxels[i].position);
    dummy.updateMatrix();
    instancedMesh.setMatrixAt(i, dummy.matrix);
}
instancedMesh.instanceMatrix.needsUpdate = true

就是这样! 请随时查看这里的完整代码:

在这里插入图片描述

4、对导入的模型进行体素化

要对导入场景的.gltf 或 .glb 等格式的模型进行体素化,我们需要以与上述相同的方式跟踪场景的每个网格。 我们将加载场景中的所有网格收集到 importedMeshes 数组中。 即使它们不会被添加到场景中进行渲染,我们仍然需要将材质面更改为 THREE.DoubleSide,以便光线投射器正确检测由内到外和由外到内的交叉点。

我们还需要重新缩放场景以适应预定义的网格大小和场景参数,例如相机、灯光和地板位置。 由于我们不知道导入模型的大小,我们将其缩放以匹配 modelSize 值并确保它以原点 (0, 0, 0) 为中心。

function voxelizeModel(importedScene) {

    const importedMeshes = [];
    importedScene.traverse((child) => {
        if (child instanceof THREE.Mesh) {
            child.material.side = THREE.DoubleSide;
            importedMeshes.push(child);
        }
    });

    // get the size of loaded model
    let boundingBox = new THREE.Box3().setFromObject(importedScene);
    const size = boundingBox.getSize(new THREE.Vector3());
    const scaleFactor = params.modelSize / size.length();
    const center = boundingBox.getCenter(new THREE.Vector3()).multiplyScalar(-scaleFactor);

    // scale model to the standard size and center it
    importedScene.scale.multiplyScalar(scaleFactor);
    importedScene.position.copy(center);

    // recalculate the box for voxelization
    boundingBox = new THREE.Box3().setFromObject(importedScene);

    // go through the grid
    for (let i = boundingBox.min.x; i < boundingBox.max.x; i += params.gridSize) {
        for (let j = boundingBox.min.y; j < boundingBox.max.y; j += params.gridSize) {
            for (let k = boundingBox.min.z; k < boundingBox.max.z; k += params.gridSize) {
                // check if XYZ position is inside one of the meshes
                for (let meshCnt = 0; meshCnt < importedMeshes.length; meshCnt++) {
                    const pos = new THREE.Vector3(i, j, k);
                    const mesh = importedMeshes[meshCnt];

                    // add pos to voxels array if it's inside the mesh
                }
            }
        }
    }
}

我们还想从加载的模型中提取颜色并将它们与 XYZ 坐标一起保存。 这可以通过收集 mesh.material.color 属性来完成。

此外,为了统一调色板,我通过将颜色转换为 HSL 颜色空间、降低饱和度和增加亮度来调整颜色。 这为来自不同导入模型的颜色创建了相似的柔和风格,在同一页面上显示时看起来更好。

// no need to specify the color for instanced mesh material
voxelMaterial = new THREE.MeshLambertMaterial({});

function voxelizeModel(importedScene) {

    // ...
    
    const mesh = importedMeshes[meshCnt];
    const color = new THREE.Color();
    const {h, s, l} = mesh.material.color.getHSL(color);
    color.setHSL(h, s * .8, l * .8 + .2);

    if (isInsideMesh(pos, mesh)) {
        voxels.push({color: color, position: pos});
    }
} 

function recreateVoxels() {
    for (let i = 0; i < voxels.length; i++) {

        // add voxel color to each instance
        instancedMesh.setColorAt(i, voxels[i].color);

        dummy.position.copy(voxels[i].position);
        dummy.updateMatrix();
        instancedMesh.setMatrixAt(i, dummy.matrix);
    }
    instancedMesh.instanceColor.needsUpdate = true; // apply the colors
    instancedMesh.instanceMatrix.needsUpdate = true;
}

很好! 我们有一个适用于导入 3D 模型的有效体素化解决方案。
在这里插入图片描述

5、已知的局限性

如果你在网上冲浪并针对不同的模型(例如我最爱的poly.pizza)尝试上面的代码,可能会看到某些模型的意外结果。 以下是可能面临的常见问题:

5.1 开放的网格

这些模型不是为体素化而设计的。 3D 艺术家在优化他们的模型和移除隐藏的面孔方面做得很好。 但是一旦隐藏的面被移除,我们就会得到开放的网格,我们的光线投射算法会产生错误的结果。 可能的解决方案是在 Blender 中封闭表面或使用光线方向和其他参数。

例如,在左图中,我们将鳄梨模型体素化,光线投射到负 Y 方向,而在右图中,光线朝向正 Y 方向。
在这里插入图片描述

5.2 薄壁网格

同一个模型在不同的体素网格下看起来可能完全不同,特别是如果模型有一些难以用网格点“捕捉”的薄元素。 调整单元格大小和边界框偏移以获得模型的良好输出——即使很小的变化也会给你带来截然不同的结果。

例如,这个鸡蛋模型的蛋白部分非常薄,所以从第一次运行开始,我们看不到它。 但是,如果你开始增加边界框的垂直偏移,可以找到一个很好的拟合并正确地对网格进行体素化。
在这里插入图片描述

5.3 复杂的材质

有时,单个网格具有多种材料的组合或在其上映射了纹理。 在这两种情况下,我们都不能采用 mesh.material.color,因此应手动设置体素颜色。

6、风格变化

在完成 3D 像素演示之前,让我们考虑一下我们可以从体素化樱桃模型中获得的其他有趣效果。
在这里插入图片描述

将体素几何形状从圆框更改为乐高积木,并沿 Y 轴调整网格步长:
在这里插入图片描述

或者将盒子切换为球体并让它们四处移动:

在这里插入图片描述

你可以通过尝试不同的体素几何体来进一步进行体素化,例如将球体设计为气泡并让它们破裂,或者用平面替换盒子几何体并在其上映射 alpha 纹理。 在之前的教程中已经创建了类似的视觉效果。

此外,请记住,你可以对来自不同网格的粒子应用不同的动画。 还可以添加物理以使体素下落和碰撞,或使它们与光标排斥。 可能性是无止境!

7、优化体素

在最后的演示中,体素被创建为大小等于网格单元格大小的框。 然而,体素是背靠背站立的,这意味着有时我们会生成一堆永远不可见的体素。 这会影响性能并增加内存使用量。

让我们考虑一下这个红苹果的体素化。 它有一些小细节,所以我们选择一个相对较小的网格。

在这里插入图片描述

体素的总数可能非常大,但其中许多位于网格内部并且永远不可见。 为了优化这一点,我们可以通过两种方式修改光线投射算法:

  • 用沿正负 X、Y 和 Z 轴的六条射线替换单条射线。
  • 保存体素坐标不仅是在网格内部(具有奇数个交点),而且如果它靠近网格表面(与第一个交点的距离很小)。
    在这里插入图片描述

体素数优化的苹果

function voxelizeModel(importedScene) {

    // ...

    for (let meshCnt = 0; meshCnt < importedMeshes.length; meshCnt++) {

        const mesh = importedMeshes[meshCnt];
        const pos = new THREE.Vector3(i, j, k);
        const color = new THREE.Color();
        const {h, s, l} = mesh.material.color.getHSL(color);
        color.setHSL(h, s * .8, l * .8 + .2);

        // this is bad for performance
        if (isInsideMesh(pos, {x: 0, y: 0, z: 1}, mesh)) {
            voxels.push({color: color, position: pos});
            break;
        } else if (isInsideMesh(pos, {x: 0, y: 0, z: -1}, mesh)) {
            voxels.push({color: color, position: pos});
            break;
        } else if (isInsideMesh(pos, {x: 0, y: 1, z: 0}, mesh)) {
            voxels.push({color: color, position: pos});
            break;
        } else if (isInsideMesh(pos, {x: 0, y: -1, z: 0}, mesh)) {
            voxels.push({color: color, position: pos});
            break;
        } else if (isInsideMesh(pos, {x: 1, y: 0, z: 0}, mesh)) {
            voxels.push({color: color, position: pos});
            break;
        } else if (isInsideMesh(pos, {x: -1, y: 0, z: 0}, mesh)) {
            voxels.push({color: color, position: pos});
            break;
        }
    }

    // ...

}

function isInsideMesh(pos, dir, mesh) {
    rayCaster.set(pos, dir);
    rayCasterIntersects = rayCaster.intersectObject(mesh, false);

    // return rayCasterIntersects.length % 2 === 1;
    return (rayCasterIntersects.length % 2 === 1 && rayCasterIntersects[0].distance <= 1.5 * params.gridSize);
}

对于这个苹果模型,我们可以在没有任何视觉变化的情况下将体素总数从 13472 减少到 3677。 另外,由于距离限制,我们可以通过这种方式处理具有平行于世界轴的开放网格和表面的模型。

但是,当使用 THREE.Raycaster 时,此方法的计算量非常大。 对于复杂的光线投射,需要边界体积层次结构 (BVH),可以使用像 three-mesh-bvh 这样的工具来管理它。 尽管如此,出于本教程的目的,我们将保持简单并使用单射线方法,其中射线朝向正 Z(因为它与所使用的模型集一起工作得更好)。

8、组合多个模型

要在同一页面上组合多个体素化模型,我们可以简单地为每个模型创建单独的容器并将它们添加到场景中。 我们还可以创建按钮或其他控件来在模型之间切换。 在这种情况下,所有模型都是从 poly.pizza 下载的 .glb 文件,并在没有任何更改的情况下使用。
在这里插入图片描述

9、多个场景

除了主场景,我们还有模型选择器。 在侧面板上,我们渲染了所有 3D 模型,每个模型都有自己的小场景。 侧面场景是相同的,我们只是更改背景颜色并添加 GLTF 模型,确保它们以相同的方式缩放和居中。 每个侧面场景都是一个单独的 THREE.Scene,具有自己的相机和轨道控件。

对于像素艺术视图,我们有一个主要的 THREE.Scene,其设置与前面的示例完全相同。

所有场景共享相同的 元素和相同的 THREE.Renderer。 THREE.Renderer 有非常方便的 .setViewport 和 .setScissor 方法来处理多个场景。 我们基本上需要:

这是一种广泛使用的技术,并不特定于体素化。 可以在这个仓库中找到完整的代码,或者也可以参考用作此方法模板的 Three.js 示例。

10、多个模型

对于体素,我们仍然可以使用单个 THREE.InstancedMesh 对象。 我们以与以前相同的方式加载、缩放和追踪所有模型到体素。 然而,我们不是将体素的位置和颜色直接存储在体素数组中,而是将它们存储在新的二维 voxelsPerModel 数组中。 体素数组现在仅保留用于渲染的当前体素。

function loadModels() {

    recreateInstancedMesh(100);

    const loader = new GLTFLoader();
    let modelsLoadCnt = 0;
    modelURLs.forEach((url, modelIdx) => {

        // prepare <div> and Three.js scene for model preview
        const scene = createPreviewScene(modelIdx);
        previewScenes.push(scene);

        // load .glb file
        loader.load(url, (gltf) => {

            // add scaled and centered model to the preview panel;
            addModelToPreview(modelIdx, gltf.scene)

            // get the voxel data from the model
            voxelizeModel(modelIdx, gltf.scene);
            
            // recreate the instanced mesh with new size
            const numberOfInstances = Math.max(...voxelsPerModel.map(m => m.length));
            if (numberOfInstances > instancedMesh.count) {
                recreateInstancedMesh(numberOfInstances);
            }

            // once all the models are loaded...
            modelsLoadCnt++;
            if (modelsLoadCnt === 1) {
                // Once we have once voxelized model ready, start rendering the available content
                updateSceneSize();
                render();
            }
            if (modelsLoadCnt === modelURLs.length) {
                // Once we have all the models voxelized, start available content
                animateVoxels(0, activeModelIdx);
                setupSelectorEvents();
            }
        }, undefined, (error) => {
            console.error(error);
        });
    })
}

在上面的代码中,我们在加载和跟踪每个下一个模型时重新创建体素数组和 THREE.InstancedMesh 元素。 或者,我们可以等待所有模型完成,使用 Math.max(…voxelsPerModel.map(m => m.length)) 检查体素总数,然后只创建一次 THREE.InstancedMesh。 但是,我们不想在计算过程中看到空白屏幕,所以在开始时显示一些随机框会更好:

function recreateInstancedMesh(cnt) {

    // remove the old mesh and voxels data
    voxels = [];
    mainScene.remove(instancedMesh);

    // re-initiate the voxel array with random colors and positions
    for (let i = 0; i < cnt; i++) {
        const randomCoordinate = () => {
            let v = (Math.random() - .5);
            v -= (v % params.gridSize);
            return v;
        }
        voxels.push({
            position: new THREE.Vector3(randomCoordinate(), randomCoordinate(), randomCoordinate()),
            color: new THREE.Color().setHSL(Math.random(), .8, .8)
        })
    }
    
    // create a new instanced mesh object
    instancedMesh = new THREE.InstancedMesh(voxelGeometry, voxelMaterial, cnt);
    instancedMesh.castShadow = true;
    instancedMesh.receiveShadow = true;

    // assign voxels data to the instanced mesh
    for (let i = 0; i < cnt; i++) {
        instancedMesh.setColorAt(i, voxels[i].color);
        dummy.position.copy(voxels[i].position);
        dummy.updateMatrix();
        instancedMesh.setMatrixAt(i, dummy.matrix);
    }
    instancedMesh.instanceMatrix.needsUpdate = true;
    instancedMesh.instanceColor.needsUpdate = true;

    // add a new mesh to the scene
    mainScene.add(instancedMesh);
}

11、动画

现在所有模型都已准备就绪,我们拥有动画所需的一切:

  • instancedMesh:每个模型都适合的网格(实例数等于最大可能的体素数)。
  • voxelsPerModel:保存每个模型的颜色和位置的数组
  • 体素:保存体素框当前颜色和位置的数组

为了将体素从一个 3D 模型动画化到另一个 3D 模型,我们使用 animateVoxels() 函数,其中所有位置属性(.x、.y 和 .z)和所有颜色属性(.r、.g 和 .b)都转换为 新的价值观。 多亏了 GSAP 库,我们可以轻松地为每个过渡设置延迟、持续时间和缓动函数。 在每个过渡帧(onUpdate 回调)中,我们更新所有网格实例的属性,就像我们之前对静态模型所做的那样。 然后,我们使用新属性更新实例化网格,并确保在过渡结束时可见正确数量的实例 (instancedMesh.count)。 我还发现在过渡时旋转模型很好。

function animateVoxels(oldModelIdx, newModelIdx) {

    // animate voxels data
    for (let i = 0; i < voxels.length; i++) {
        
        gsap.killTweensOf(voxels[i].color);
        gsap.killTweensOf(voxels[i].position);

        const duration = .6 + .6 * Math.pow(Math.random(), 6);
        let targetPos;

        // move to new position if we have one;
        // otherwise, move to a randomly selected existing position
        //
        // animate to new color if it's determined
        // otherwise, voxel will be just hidden by animation of instancedMesh.count

        if (voxelsPerModel[newModelIdx][i]) {
            targetPos = voxelsPerModel[newModelIdx][i].position;
            gsap.to(voxels[i].color, {
                delay: .7 * Math.random() * duration,
                duration: .05,
                r: voxelsPerModel[newModelIdx][i].color.r,
                g: voxelsPerModel[newModelIdx][i].color.g,
                b: voxelsPerModel[newModelIdx][i].color.b,
                ease: "power1.in",
                onUpdate: () => {
                    instancedMesh.setColorAt(i, voxels[i].color);
                }
            })
        } else {
            targetPos = voxelsPerModel[newModelIdx][Math.floor(voxelsPerModel[newModelIdx].length * Math.random())].position;
        }

        // move to new position if it's determined
        gsap.to(voxels[i].position, {
            delay: .2 * Math.random(),
            duration: duration,
            x: targetPos.x,
            y: targetPos.y,
            z: targetPos.z,
            ease: "back.out(3)",
            onUpdate: () => {
                dummy.position.copy(voxels[i].position);
                dummy.updateMatrix();
                instancedMesh.setMatrixAt(i, dummy.matrix);
            }
        });
    }

    // increase the model rotation during transition
    gsap.to(instancedMesh.rotation, {
        duration: 1.2,
        y: "+=" + 1.3 * Math.PI,
        ease: "power2.out"
    })

    // show the right number of voxels
    gsap.to(instancedMesh, {
        duration: .4,
        count: voxelsPerModel[newModelIdx].length
    })

    // update the instanced mesh accordingly to voxels data
    // (no need to call it per each voxel)
    gsap.to({}, {
        duration: 1.5, // max transition duration
        onUpdate: () => {
            instancedMesh.instanceColor.needsUpdate = true;
            instancedMesh.instanceMatrix.needsUpdate = true;
        }
    });
}

其余的是关于用户界面。 我们注册预览上的点击以动画化到选定的模型中。 通过单击屏幕上的其他位置,我们将体素动画化到下一个模型。 为了管理点击,我们使用 mouseup/mousedown 事件和鼠标保持超时的组合,而不是点击事件。 它有助于避免模型选择和轨道控制事件之间的冲突。 你可以在 repo 中找到完整的代码。


原文链接:Three.js体素化原理及实现 — BimAnt

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

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

相关文章

SES2000浅地层剖面仪自带处理软件ISE2.95的处理步骤

SES2000是目前市面上主流浅地层剖面仪。它的自带处理软件ISE经常和设备一起更新&#xff0c;造成ISE版本众多&#xff0c;虽然数据采集的格式都是raw&#xff0c;但是低版本ISE软件打不开高版本raw数据&#xff0c;即使软件版本相近&#xff0c;比如都是2.95版本序列&#xff0…

AI测试|天猫精灵智能音箱测试策略与方法

一、业务介绍 2014年11月&#xff0c;亚马逊推出了一款全新概念的智能音箱&#xff1a;Echo&#xff0c;这款产品最大的亮点是将智能语音交互技术植入到传统音箱中&#xff0c;从而赋予了音箱人工智能的属性。这个被称为“Alexa”的语音助手可以像你的朋友一样与你交流&#x…

Grafana系列-统一展示-9-Jaeger数据源

系列文章 Grafana 系列文章 配置 Jaeger data source Grafana内置了对Jaeger的支持&#xff0c;它提供了开源的端到端分布式跟踪。本文解释了针对Jaeger数据源的配置和查询。 关键的配置如下: URL: Jaeger 实例的 URL, 如: http://localhost:16686 或 http://localhost:16…

PPT技能之新手入门,零基础光速进阶的宝藏

不会PPT只是借口&#xff0c;懒惰才是你的心里话。只要现在开始学习&#xff0c;不出三个月&#xff0c;华丽蜕变成PPT大神&#xff01;你的进步&#xff0c;我的功劳&#xff01; 你的关注&#xff0c;是我最大的动力&#xff01;你的转发&#xff0c;我的10W&#xff01;茫茫…

维京人的秘密:残暴背后的真相,敬畏神灵死后进入英灵殿

维京人&#xff0c;一个充满神秘色彩的名字&#xff0c;勾起了人们对于古代北欧残暴战士的想象。然而&#xff0c;维京人究竟是如何形成这样的形象&#xff0c;他们的传统和习俗又是如何塑造了他们的一生呢&#xff1f; 首先&#xff0c;我们要了解维京人的生活背景。维京人生活…

Linux线程同步(5)——互斥锁or自旋锁?

自旋锁概述 自旋锁与互斥锁很相似&#xff0c;从本质上说也是一把锁&#xff0c;在访问共享资源之前对自旋锁进行上锁&#xff0c;在访问完成后释放自旋锁&#xff08;解锁&#xff09;&#xff1b;事实上&#xff0c;从实现方式上来说&#xff0c;互斥锁是基于自旋锁…

shell脚本之“sort“、“uniq“、“tr“、“cut“、“split“、“paste“以及“eval“命令详解

文章目录 sort命令uniq命令tr命令cut命令split命令paste命令eval命令总结 sort命令 以行为单位对文件内容进行排序&#xff0c;也可以根据不同的数据类型来排序. 比较原则&#xff1a;从首字符向后&#xff0c;依次按ASCII码值进行比较&#xff0c;最后将他们按升序输出. 语法…

Chrome和edge报STATUS_STACK_BUFFER_OVERRUN错误的处理办法

Chrome和edge突然就报STATUS_STACK_BUFFER_OVERRUN错误&#xff0c;原因未知。 解决方案&#xff1a; Chrome 卸载本地的chrome访问https://www.chromedownloads.net/chrome64win/&#xff08;windows64&#xff09;https://www.chromedownloads.net/chrome32win/&#xff0…

母亲节到了,写一个简单的C++代码给老妈送上一个爱心祝福

&#x1f34e; 博客主页&#xff1a;&#x1f319;披星戴月的贾维斯 &#x1f34e; 欢迎关注&#xff1a;&#x1f44d;点赞&#x1f343;收藏&#x1f525;留言 &#x1f347;系列专栏&#xff1a;&#x1f319; C/C专栏 &#x1f319;请不要相信胜利就像山坡上的蒲公英一样唾…

快速上手Arthas

目录 基本概述 安装方式 基础指令 jvm相关指令 class/classloader相关指令 monitor/watch/trace相关指令 其他 基本概述 jconsole等工具都必须在服务端项目进程中配置相关的监控参数&#xff0c;然后工具通过远程连接到项目进程&#xff0c;获取相关的数据。这样就会带…

快速查询的秘籍——B+树索引

页和记录的关系示意图 InnoDB根据主键查找数据的过程是什么&#xff1f; 没有索引的查找是什么&#xff1f;索引查找和通过主键查找有什么关系&#xff1f; 索引是解决什么问题的&#xff1f; 索引是解决定位数据页的&#xff0c;而不是定位一个页中的数据的&#xff0c;定位…

MATLAB绘制动画(一)质点动画

vx 100*cos(1/3*pi); vy 100*sin(1/3*pi); t 0:0.005:18; x vx*t; y vy*-9.8*t.^2/2; comet(x,y) 这里只是截取了最后的画面&#xff0c;正常运行时&#xff0c;可以看到从最高点向下落的动作。 想要了解这段代码&#xff0c;我们要知道comet函数的意义 这个函数可以沿着…

ChatGPT 发布重磅更新,插件系统即将上线!

公众号关注 “GitHubDaily” 设为 “星标”&#xff0c;每天带你逛 GitHub&#xff01; 昨天凌晨&#xff0c;ChatGPT 为诸多 Plus 会员陆续开放了插件系统内测权限&#xff0c;申请比较早的用户&#xff0c;现在应该都能体验上最新的插件系统了。 为了让风暴来得更为猛烈&…

SQL在线刷题

牛客网学习SQL在线编程&#xff0c;牛客网在线编程&#xff0c;一共82道 用于实践的网站&#xff0c;在线运行SQL 目前43道&#xff0c;刷不动了&#xff0c;剩下的之后找机会搞 只记录有疑问的题目 简单 SQL196 查倒数第三 查找入职员工时间排名倒数第三的员工所有信息 …

js堆和栈

目录 关键句提取&#xff1a; 一、认识堆和栈 1、内存操作场景 2、数据结构场景 二、堆和栈的优缺点 1.栈(stack) 2.堆(heap) 3.总结&#xff1a; 三、堆和栈的溢出 四、 传值和传址 五、为什么会有栈内存和堆内存之分&#xff1f; 垃圾回收 标记清理 引用…

品牌控价的好处有哪些、品牌控价方法有哪些

今天和大家聊聊【品牌控价】&#xff0c;他们常会说到自己的产品有多好&#xff0c;经销商们体验完也说产品效果非常不错&#xff0c;价格在业内也是有很大优势&#xff0c;但是客户购买量和预期效果确有很大差距&#xff0c;难道我产品性价比这个高&#xff0c;还不能打动顾客…

一种不需要注册没有魔法使用ChatGPT的方法

关于我&#xff1a;关注AIGC、读书、成长和自媒体。加我微信&#xff1a;keeepdance&#xff0c;备注&#xff1a;chatgpt。进ChatGPT交流群。 如果你还没有使用过ChatGPT&#xff0c;那你来对了地方。文章结尾&#xff0c;我将提供一种能不需要梯子、不需要注册&#xff0c;无…

【数据结构.C】顺序表和单链表的增删查改

宝子&#xff0c;你不点个赞吗&#xff1f;不评个论吗&#xff1f;不收个藏吗&#xff1f; 最后的最后&#xff0c;关注我&#xff0c;关注我&#xff0c;关注我&#xff0c;你会看到更多有趣的博客哦&#xff01;&#xff01;&#xff01; 喵喵喵&#xff0c;你对我真的很重要…

干货! CVPR:基于VDB的高效神经辐射渲染场

点击蓝字 关注我们 AI TIME欢迎每一位AI爱好者的加入&#xff01; 作者介绍 严 涵 上海交通大学2019级人工智能专业本科生&#xff0c;研究兴趣主要是与NeRF相关的三维重建算法。 报告题目 基于VDB高效神经辐射渲染场 内容简介 01 NeRF NeRF的提出起初主要是为了解决新视⻆生成…

shell脚本常用的命令

管理文件内容的使用 一、sort命令二、uniq命令三、tr命令四、cut命令五、split命令六、paste命令七、eval命令 一、sort命令 sort命令是以行为单位对文件内容进行排序&#xff0c;也可以根据不同的数据类型来排序&#xff0c;比较原则是从首字符向后&#xff0c;依次按ASCII码…