Three.js铅笔手绘效果实现

news2024/11/30 18:36:28

在这个教程中,我们将学习如何使用 Three.js 后处理创建铅笔手绘效果。 我们将完成创建自定义后处理渲染通道、在 WebGL 中实现边缘检测、将法线缓冲区重新渲染到渲染目标以及使用生成和导入的纹理调整最终结果的步骤。

这就是最终结果的样子,让我们开始吧!
在这里插入图片描述

推荐:将 NSDT场景编辑器 加入你的3D开发工具链。

1、Three.js 中的后处理

Three.js 中的后处理是一种在绘制场景后将效果应用于渲染场景的方法。 除了 Three.js 提供的所有开箱即用的后处理效果外,还可以通过创建自定义渲染通道来添加您自己的滤镜。

自定义渲染过程本质上是一个函数,它接收场景图像并返回一个新图像,并应用所需的效果。 你可以将这些渲染通道想象成 Photoshop 中的图层效果——每个渲染通道都基于之前的效果输出应用新的滤镜。 生成的图像是所有不同效果的组合。

2、在 Three.js 中启用后处理

要向我们的场景添加后处理,需要设置场景渲染在 WebGLRenderer之外还使用 EffectComposer。 效果器合成器将后处理效果按传递顺序堆叠在一起。 如果我们想让渲染场景传递给下一个效果,需要先添加RenderPass后处理pass传递。

然后,在启动渲染循环的 tick 函数中,我们调用 composer.render() 而不是 renderer.render(scene, camera)。

const renderer = new THREE.WebGLRenderer()
// ... settings for the renderer are available in the Codesandbox below

const composer = new EffectComposer(renderer)
const renderPass = new RenderPass(scene, camera)

composer.addPass(renderPass)

function tick() {
	requestAnimationFrame(tick)
	composer.render()
}

tick()

有两种创建自定义后处理效果的方法:

  • 创建自定义着色器并将其传递给 ShaderPass 实例,或者
  • 通过扩展 Pass 类创建自定义渲染通道。

因为我们希望我们的后处理效果获得比制服和属性更多的信息,所以我们将创建一个自定义渲染通道。

3、创建自定义渲染通道

虽然目前没有太多关于如何在 Three.js 中编写您自己的自定义后处理通道的文档,但库中已有大量示例可供学习。 自定义通道继承自通道类,具有三个方法:setSize、render 和dispose。 正如您可能已经猜到的那样,我们将主要关注渲染方法。

首先,我们将从创建自己的 PencilLinesPass 开始,它扩展了 Pass 类,稍后将实现我们自己的渲染逻辑。

import { Pass, FullScreenQuad } from 'three/examples/jsm/postprocessing/Pass'
import * as THREE from 'three'

export class PencilLinesPass extends Pass {
	constructor() {
		super()
	}

	render(
		renderer: THREE.WebGLRenderer,
		writeBuffer: THREE.WebGLRenderTarget,
		readBuffer: THREE.WebGLRenderTarget
	) {
		if (this.renderToScreen) {
			renderer.setRenderTarget(null)
		} else {
			renderer.setRenderTarget(writeBuffer)
			if (this.clear) renderer.clear()
		}
	}
}

如你所见,render 方法接受一个 WebGLRenderer 和两个 WebGLRenderTargets,一个用于写入缓冲区,另一个用于读取缓冲区。 在 Three.js 中,渲染目标基本上是我们可以渲染场景的纹理,它们用于在通道之间发送数据。 读取缓冲区从先前的渲染通道接收数据,在我们的例子中是默认的渲染通道。 写入缓冲区将数据发送到下一个渲染通道。

如果 renderToScreen 为真,则意味着我们要将缓冲区发送到屏幕而不是渲染目标。 渲染器的渲染目标设置为 null,因此它默认为屏幕画布。

在这一点上,我们实际上并没有渲染任何东西,甚至没有通过 readBuffer 传入的数据。 为了渲染事物,我们需要创建一个 FullscreenQuad 和一个负责渲染的着色器材质。 着色器材质被渲染到 FullscreenQuad。

为了测试一切设置是否正确,我们可以使用内置的 CopyShader 来显示我们放入其中的任何图像。 在这种情况下,在这种情况下是 readBuffer 的纹理。

import { Pass, FullScreenQuad } from 'three/examples/jsm/postprocessing/Pass'
import { CopyShader } from 'three/examples/jsm/shaders/CopyShader'
import * as THREE from 'three'

export class PencilLinesPass extends Pass {
	fsQuad: FullScreenQuad
	material: THREE.ShaderMaterial

	constructor() {
		super()

		this.material = new THREE.ShaderMaterial(CopyShader)
		this.fsQuad = new FullScreenQuad(this.material)
	}

	dispose() {
		this.material.dispose()
		this.fsQuad.dispose()
	}

	render(
		renderer: THREE.WebGLRenderer,
		writeBuffer: THREE.WebGLRenderTarget,
		readBuffer: THREE.WebGLRenderTarget
	) {
		this.material.uniforms['tDiffuse'].value = readBuffer.texture

		if (this.renderToScreen) {
			renderer.setRenderTarget(null)
			this.fsQuad.render(renderer)
		} else {
			renderer.setRenderTarget(writeBuffer)
			if (this.clear) renderer.clear()
			this.fsQuad.render(renderer)
		}
	}
}

注意:我们将统一的 tDiffuse 传递给着色器材质。 CopyShader 已经内置了这种制服,它代表要在屏幕上显示的图像。 如果你正在编写自己的 ShaderPass,此uniform将自动传递到您的着色器。

剩下的就是通过将自定义渲染通道添加到 EffectComposer 来将自定义渲染通道连接到场景中——当然是在 RenderPass 之后!

const renderPass = new RenderPass(scene, camera)
const pencilLinesPass = new PencilLinesPass()

composer.addPass(renderPass)
composer.addPass(pencilLinesPass)

在这里插入图片描述

现在我们已经设置好了一切,我们实际上可以开始创建我们的特殊效果了!

4、用于创建轮廓的 Sobel 算子

我们需要能够告诉计算机根据我们的输入图像检测线条,在本例中是渲染场景。 我们将使用的这种边缘检测称为 Sobel 算子,它只包含几个步骤。

Sobel 算子通过观察图像一小部分的梯度来进行边缘检测——本质上是从一个值到另一个值的过渡有多尖锐。 图像被分解成更小的“内核”,或 3px x 3px 的正方形,其中中心像素是当前正在处理的像素。 下图显示了它的样子:中心的红色方块代表当前正在评估的像素,其余方块是它的邻居。

在这里插入图片描述

3 x 3px 核

然后通过获取像素值(亮度)并将其乘以基于其相对于被评估像素的位置的权重来计算每个邻居的加权值。 这是通过权重在水平和垂直方向上偏置梯度来完成的。 取两个值的平均值,如果它超过某个阈值,我们认为该像素表示边缘。
在这里插入图片描述

Sobel 算子的水平和垂直梯度

虽然 Sobel 算子的实现几乎直接遵循上面的图像表示,但仍然需要时间来掌握。 值得庆幸的是,我们不必自己实现,因为 Three.js 已经为我们提供了 SobelOperatorShader 中的代码。 我们会将这段代码复制到我们的着色器材质中。

5、实现 Sobel 算子

我们现在需要添加自己的 ShaderMaterial 而不是 CopyShader,以便我们可以控制顶点和片段着色器,以及发送到这些着色器的uniform。

// PencilLinesMaterial.ts
export class PencilLinesMaterial extends THREE.ShaderMaterial {
	constructor() {
		super({
			uniforms: {
				// we'll keep the naming convention here since the CopyShader
				// also used a tDiffuse texture for the currently rendered scene.
				tDiffuse: { value: null },
				// we'll pass in the canvas size here later
				uResolution: {
					value: new THREE.Vector2(1, 1)
				}
			},
			fragmentShader, // to be imported from another file
			vertexShader // to be imported from another file
		})
	}
}

我们很快就会接触到片段和顶点着色器,但首先我们需要在场景中使用我们的新着色器材质。 我们通过换出 CopyShader 来做到这一点。 不要忘记将分辨率(画布大小)作为着色器的uniform传递。 虽然超出了本教程的范围,但在画布调整大小时更新此uniform也很重要。

// PencilLinesPass.ts
export class PencilLinesPass extends Pass {
	fsQuad: FullScreenQuad
	material: PencilLinesMaterial

	constructor({ width, height }: { width: number; height: number }) {
		super()
		
		// change the material from to our new PencilLinesMaterial
		this.material = new PencilLinesMaterial() 
		this.fsQuad = new FullScreenQuad(this.material)

		// set the uResolution uniform with the current canvas's width and height
		this.material.uniforms.uResolution.value = new THREE.Vector2(width, height)
	}
}

接下来,我们可以从顶点和片段着色器开始。

除了设置 gl_Position 值并将 uv 属性传递给片段着色器外,顶点着色器并没有做太多事情。 因为我们将图像渲染到 FullscreenQuad,所以 uv 信息对应于任何给定片段在屏幕上的位置。

// vertex shader
varying vec2 vUv;

void main() {

    vUv = uv;

    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}

片段着色器要复杂一些,所以让我们逐行分解。 首先,我们要使用 Three.js 已经提供的实现来实现 Sobel 运算符。 唯一的区别是我们想要控制我们如何计算每个像素的值,因为我们也将引入正常缓冲区的线检测。

float combinedSobelValue() {
    // kernel definition (in glsl matrices are filled in column-major order)
    const mat3 Gx = mat3(-1, -2, -1, 0, 0, 0, 1, 2, 1);// x direction kernel
    const mat3 Gy = mat3(-1, 0, 1, -2, 0, 2, -1, 0, 1);// y direction kernel

    // fetch the 3x3 neighbourhood of a fragment

    // first column
    float tx0y0 = getValue(-1, -1);
    float tx0y1 = getValue(-1, 0);
    float tx0y2 = getValue(-1, 1);

    // second column
    float tx1y0 = getValue(0, -1);
    float tx1y1 = getValue(0, 0);
    float tx1y2 = getValue(0, 1);

    // third column
    float tx2y0 = getValue(1, -1);
    float tx2y1 = getValue(1, 0);
    float tx2y2 = getValue(1, 1);

    // gradient value in x direction
    float valueGx = Gx[0][0] * tx0y0 + Gx[1][0] * tx1y0 + Gx[2][0] * tx2y0 +
    Gx[0][1] * tx0y1 + Gx[1][1] * tx1y1 + Gx[2][1] * tx2y1 +
    Gx[0][2] * tx0y2 + Gx[1][2] * tx1y2 + Gx[2][2] * tx2y2;

    // gradient value in y direction
    float valueGy = Gy[0][0] * tx0y0 + Gy[1][0] * tx1y0 + Gy[2][0] * tx2y0 +
    Gy[0][1] * tx0y1 + Gy[1][1] * tx1y1 + Gy[2][1] * tx2y1 +
    Gy[0][2] * tx0y2 + Gy[1][2] * tx1y2 + Gy[2][2] * tx2y2;

    // magnitude of the total gradient
    float G = (valueGx * valueGx) + (valueGy * valueGy);
    return clamp(G, 0.0, 1.0);
}

我们将当前像素的偏移量传递给 getValue 函数,从而确定我们正在查看内核中的哪个像素以获取该值。 目前,仅评估漫反射缓冲区的值,我们将在下一步中添加普通缓冲区。

float valueAtPoint(sampler2D image, vec2 coord, vec2 texel, vec2 point) {
    vec3 luma = vec3(0.299, 0.587, 0.114);

    return dot(texture2D(image, coord + texel * point).xyz, luma);
}

float diffuseValue(int x, int y) {
    return valueAtPoint(tDiffuse, vUv, vec2(1.0 / uResolution.x, 1.0 / uResolution.y), vec2(x, y)) * 0.6;
}

float getValue(int x, int y) {
    return diffuseValue(x, y);
}

valueAtPoint 函数采用任何纹理(漫反射或法线)并返回指定点的灰度值。 亮度向量用于计算颜色的亮度,从而将颜色转换为灰度。 实现来自 glsl-luma。

因为 getValue 函数只考虑漫反射缓冲区,这意味着场景中的任何边缘都将被检测到,包括由投射阴影和核心阴影创建的边缘。 这也意味着我们会凭直觉感知到的边缘,例如物体的轮廓,如果它们与周围环境融合得太好,可能会被忽略。 为了捕获那些缺失的边缘,我们接下来将从普通缓冲区添加边缘检测。

最后,我们在主函数中调用 Sobel 运算符,如下所示:

void main() {
    float sobelValue = combinedSobelValue();
    sobelValue = smoothstep(0.01, 0.03, sobelValue);

    vec4 lineColor = vec4(0.32, 0.12, 0.2, 1.0);

    if (sobelValue > 0.1) {
        gl_FragColor = lineColor;
    } else {
        gl_FragColor = vec4(1.0);
    }
}

在这里插入图片描述

使用 Sobel 算子进行边缘检测的渲染场景

6、创建法线缓冲区渲染

为了获得合适的轮廓,Sobel 算子通常应用于场景的法线和深度缓冲区,因此会捕获对象的轮廓,但不会捕获对象内的线条。 Omar Shehata 在他出色的 How to render outlines in WebGL 教程中描述了这种方法。 出于粗略铅笔效果的目的,我们不需要完整的边缘检测,但我们确实希望使用法线来获得更完整的边缘和稍后的粗略阴影效果。

由于法线是表示对象表面每个点方向的向量,因此通常用颜色表示以获取包含场景中所有法线数据的图像。 此图像是“法线缓冲区”。

为了创建普通缓冲区,首先我们需要在 PencilLinesPass 构造函数中创建一个新的渲染目标。 我们还需要在该类上创建一个 MeshNormalMaterial,因为我们将在渲染法线缓冲区时使用它来覆盖场景的默认材质。

const normalBuffer = new THREE.WebGLRenderTarget(width, height)

normalBuffer.texture.format = THREE.RGBAFormat
normalBuffer.texture.type = THREE.HalfFloatType
normalBuffer.texture.minFilter = THREE.NearestFilter
normalBuffer.texture.magFilter = THREE.NearestFilter
normalBuffer.texture.generateMipmaps = false
normalBuffer.stencilBuffer = false
this.normalBuffer = normalBuffer

this.normalMaterial = new THREE.MeshNormalMaterial()

为了渲染通道内的场景,渲染通道实际上需要对场景和相机的引用。 我们还需要通过渲染通道的构造函数发送它们。

// PencilLinesPass.ts constructor
constructor({ ..., scene, camera}: { ...; scene: THREE.Scene; camera: THREE.Camera }) {
	super()
	this.scene = scene
	this.camera = camera
    ...
}

在 pass 的渲染方法中,我们想要用覆盖默认材质的普通材质重新渲染场景。 我们将 renderTarget 设置为 normalBuffer 并像往常一样使用 WebGLRenderer 渲染场景。 唯一的区别是,渲染器不是使用场景的默认材质渲染到屏幕,而是使用普通材质渲染到我们的渲染目标。 然后我们将 normalBuffer.texture 传递给着色器材质。

renderer.setRenderTarget(this.normalBuffer)
const overrideMaterialValue = this.scene.overrideMaterial

this.scene.overrideMaterial = this.normalMaterial
renderer.render(this.scene, this.camera)
this.scene.overrideMaterial = overrideMaterialValue

this.material.uniforms.uNormals.value = this.normalBuffer.texture
this.material.uniforms.tDiffuse.value = readBuffer.texture

如果此时要使用 texture2D(uNormals, vUv) 将 gl_FragColor 设置为法线缓冲区的值; 这将是结果:
在这里插入图片描述

当前场景的法线缓冲区

相反,在自定义材质的片段着色器中,我们想要修改 getValue 函数以包含来自普通缓冲区的 Sobel 运算符值。

float normalValue(int x, int y) {
    return valueAtPoint(uNormals, vUv, vec2(1.0 / uResolution.x, 1.0 / uResolution.y), vec2(x, y)) * 0.3;
}

float getValue(int x, int y) {
    return diffuseValue(x, y) + normalValue(x, y);
}

结果看起来与上一步相似,但我们将能够在下一步中使用此法线数据添加额外的噪声和粗略度。

在这里插入图片描述

应用于漫反射和法线缓冲区的 Sobel 算子

7、为阴影和波浪线添加生成的纹理噪声

此时有两种方法可以将噪声带入后处理效果:

  • 通过在着色器中按程序生成噪声,或者
  • 通过使用带有噪声的现有图像并将其应用为纹理。

两者都提供不同级别的灵活性和控制。 对于噪声函数,我使用了 Inigo Quilez 的梯度噪声实现,因为它在应用于“阴影”效果时提供了很好的噪声均匀性”。

这个噪声函数是在获取Sobel算子的值时调用的,专门作用于法线值,所以片段着色器中的getValue函数变化如下:

float getValue(int x, int y) {
    float noiseValue = noise(gl_FragCoord.xy);
    noiseValue = noiseValue * 2.0 - 1.0;
    noiseValue *= 10.0;

    return diffuseValue(x, y) + normalValue(x, y) * noiseValue;
}

结果是在法向量值发生变化的对象曲线上形成带纹理的铅笔线和点画效果。 请注意,平面对象(如飞机)不会产生这些效果,因为它们的法线值没有任何变化。

此效果的下一步也是最后一步是为线条添加失真。 为此,我使用了在 Photoshop 中使用渲染云效果创建的纹理文件。
在这里插入图片描述

在 Photoshop 中创建的生成的云纹理

云纹理通过uniform变量传递给着色器,与漫反射和法线缓冲区的方式相同。 一旦着色器可以访问纹理,我们就可以对每个片段的纹理进行采样,并使用它来偏移我们在缓冲区中读取的位置。 本质上,我们通过扭曲我们正在阅读的图像来获得波浪线效果,而不是通过绘制到不同的地方。 因为纹理的噪点是平滑的,线条不会出现锯齿状和不规则。

float normalValue(int x, int y) {
    float cutoff = 50.0;
    float offset = 0.5 / cutoff;
    float noiseValue = clamp(texture(uTexture, vUv).r, 0.0, cutoff) / cutoff - offset;

    return valueAtPoint(uNormals, vUv + noiseValue, vec2(1.0 / uResolution.x, 1.0 / uResolution.y), vec2(x, y)) * 0.3;
}

你还可以研究如何单独应用每个缓冲区的效果。 这会导致线条相互偏移,从而产生更好的手绘效果。

在这里插入图片描述

最终效果包括基于正常缓冲区的“阴影”和线条失真

8、结束语

有许多技术可以在 3D 中创建手绘或素描效果,本教程仅列出其中的一部分。 从这里开始,有多种方法可以前进。 可以通过基于噪声纹理调制被认为是边缘的阈值来调整线条粗细。 还可以将 Sobel 运算符应用于深度缓冲区,完全忽略漫反射缓冲区,以获得没有轮廓阴影的轮廓对象。 此外,还可以根据场景中的照明信息而不是基于对象的法线来添加生成的噪声。 可能性是无限的,我希望本教程能激励你深入研究!


原文链接:Three铅笔手绘效果 — BimAnt

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

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

相关文章

NLP学习笔记(九) 分词(上)

大家好,我是半虹,这篇文章来讲分词算法 1 概述 分词是自然语言处理领域中的基础任务,是文本预处理的重要步骤 简单来说,就是将文本段落分解为基本语言单位,亦可称之为词元 ( token\text{token}token ) 按照粒度的不…

day50【代码随想录】动态规划之不同的子序列、两个字符串的删除操作、编辑距离

文章目录前言一、不同的子序列(力扣115)【hard】二、两个字符串的删除操作(力扣583)思路一思路二三、编辑距离(力扣72)【hard】前言 1、不同的子序列 2、两个字符串的删除操作 3、编辑距离 一、不同的子序…

网页扫描图像并以pdf格式上传到服务器端

本文描述如何通过网页驱动扫描仪、高拍仪等图像扫描设备进行图像扫描,扫描结果经编辑修改后以pdf压缩格式上传到后台java程序中进行服务器端落盘保存。图像扫描上传如文字描述顺序所介绍,先要驱动扫描设备工作,进行纸张数据的光学扫描操作形成…

Impacket工具使用

Impacket工具说明 Impacker是用户处理网络协议的Python类集合,用于对SAB1-3或IPv4/IPv6 上的TCP/UPD/ICMP/IGMP/ARP/IPv4/IPv6/SMB/MSRPC/NTLM/Kerberos/WMI/LDAP 等进行低级的编程访问,数据包可以从头开始构建,也可以从原始数据包中解析, 面向对象API使用处理协议的深层结构变…

Action Segmentation数据集介绍——Breakfast

文章目录简介细节Cooking actibitiesillustration of the actions论文讲解Breakfast(The Breakfast Action Dataset)简介 早餐动作数据集包括与早餐准备相关的10个动作,由18个不同厨房的52个不同的人执行。该数据集是最大的完全带注释的数据…

CCIE 350-401-实验全

实验 VRRP(hello packettimes advertise) This is a lab item in which tasks will be performed on virtual devices. Refer to the Tasks tab to view the tasks for this lab item.Refer to the Topology tab to access the device console(s) and p…

Spring Boot自动装配的原理

Spring Boot自动装配的原理自动装配的实现EnableAutoConfigurationAutoConfigurationImportSelectorSpring Boot中的自动装配,它是Starter的基础,也是Spring Boot的核心。那么什么叫自动装配呢?或者说什么叫装配呢? 简单来说&…

Vulnhub靶场----4、DC-4

文章目录一、环境搭建二、渗透流程三、思路总结一、环境搭建 DC-4下载地址:https://download.vulnhub.com/dc/DC-4.zip kali:192.168.144.148 DC-4:192.168.144.152 二、渗透流程 端口扫描:nmap -T5 -p- -sV -sT -A 192.168.144.1…

ISIS协议

ISIS协议基础简介应用场景路由计算过程地址结构路由器分类邻居Hello报文邻居关系建立DIS及DIS与DR的类比链路状态信息的载体链路状态信息的交互路由算法网络分层路由域![在这里插入图片描述](https://img-blog.csdnimg.cn/9027c43b614a4399ae1f54e87a37f047.png)区域间路由简介…

【MySQL】表操作和库操作

文章目录概念库操作1.创建数据库2.删除数据库3.选择数据库4.显示数据库列表表操作1.创建数据表CREATE2.删除数据表DROP3.插入数据INSERT4.更新数据UPDATE5.修改数据ALTER6.查询数据SELECT7.WHERE子句8.ORDER BY子句9.LIMIT子句10.GROUP BY子句11.HAVING子句使用注意事项概念 M…

Java集合(一)---List和set

1.Java集合有哪些?集合类型主要有3种:set(集)、list(列表)和map(映射)Map接口和Collection接口是所有集合框架的父接口:1. Collection接口的子接口包括:Set接口和List接口2. Map接口的实现类主要有&#xf…

【Python】批量采集原神表情包~

嗨害大家好鸭~我是小熊猫(✿◡‿◡) 最近迷上了原神, 不自觉中就很喜欢保存广大旅行者制作的表情包~ 真的很有意思诶~ 源码资料电子书:点击此处跳转文末名片获取 一个个保存的话,好像效率很低嘛… 那我就发挥我小熊猫的老本行直接给把他们全部采集下…

零基础机器学习做游戏辅助第十三课--原神自动钓鱼(三)labelimg的使用

一、什么是labelimg labelimg是一款开源的图像标注工具,标签可用于分类和目标检测,它是用python写的,并使用Qt作为其图形界面,简单好用(虽然是英文版的)。其注释以 PASCAL VOC格式保存为XML文件,这是ImageNet使用的格式。此外,它还支持 COCO数据集格式。 二、安装label…

大数据时代下的企业网络安全

在大数据技术迅猛发展的今天,网络安全问题已经发展成一个广受关注的热门研究方向。有人说,“大数据下,人人裸奔”,隐私保护、数据防护日益成为广大学者、企业研究的焦点。 面对这种安全威胁,企业必须实施一些有效的信…

Allegro无法看到金属化孔的钻孔的原因和解决办法

Allegro无法看到金属化孔的钻孔的原因和解决办法 用Allegro设计PCB的时候,希望同时看到金属化孔的盘以及钻孔,如下图 但是有时显示效果是这样的,看不到钻孔了 导致无法直观地区分是通孔是还是表贴的盘 如何解决,具体操作如下 点击Setup

27正定矩阵

这一节进入正定矩阵的内容,什么叫做正定矩阵?为什么我们对矩阵正定这么感兴趣? PS:这一节将前面所有的概念都融合在一起:主元、行列式、特征值、不稳定性 一、正定矩阵的判断方法 为了说明问题,我们先考…

低代码平台和无代码平台哪个更适合开发企业管理系统?

编者按:本文分析了开发企业管理系统所需要的平台特性,并根据这些特点和低代码无代码的优劣比较,得出低代码平台更适合开发企业管理系统。关键词:私有化部署,可视化设计,源码交付,数据集成&#…

研究人员在野外发现大量的信息窃取者 “Stealc “的样本

一个名为Stealc的新信息窃取者正在暗网上做广告,它可能成为其他同类恶意软件的一个值得竞争的对象。 "SEKOIA在周一的一份报告中说:"威胁行为者将Stealc作为一个功能齐全、随时可用的窃取者,其开发依赖于Vidar、Raccoon、Mars和Re…

LeetCode 73. 矩阵置零

LeetCode 73. 矩阵置零 难度:middle\color{orange}{middle}middle 题目描述 给定一个 KaTeX parse error: Double subscript at position 3: _m_̲ x _n_ 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法…

Power Query 再谈日期表

Power Query 再谈日期表 需要附件的朋友可以点赞关注后,在公众号(焦棚子)后台回复 日期表 三个字即可领取。 一、背景 在 Power BI 或者 Power Pivot 做业务分析中,必不可少的就是日期表。我们继续优化调制出符合国人习惯日期表。 本次更新&#xff…