Unity URP 如何写基础的曲面细分着色器

左边是默认Cube在网格模式下经过曲面细分的结果,右边是原状态。

曲面细分着色器在顶点着色器、几何着色器之后,像素着色器之前。

它的作用时根据配置信息生成额外的顶点以切割原本的面片。

关于这部分有一个详细的英文教程,感兴趣可以看一下。

https://catlikecoding.com/unity/tutorials/advanced-rendering/tessellation/

以下是完整代码

Shader "Kerzh/KerzhCgShaderTemplate"
{
    Properties
    {
	    _Color("Color", Color) = (1,1,1,1)
		_TessellationUniform ("Tessellation Uniform", Vector) = (1,1,1,1)
    }
    
    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalRenderPipeline" }

        Pass
        {
        	CGPROGRAM
		    #pragma vertex vert
			#pragma hull hull
			#pragma domain domain
		    #pragma geometry geom
			#pragma fragment frag
			#pragma target 4.6
            
			#include "UnityCG.cginc"
			#include  "Assets/TA/ShaderLib/CgincInclude//CommonCgInc.cginc"
			#include  "Assets/TA/ShaderLib/CgincInclude//CustomTessellation.cginc"
            
			MeshData vert (MeshData input)
            {
                return input;
            }

			//如果不正确配置会报错
			[UNITY_domain("tri")]  //  正在处理三角形   "tri", "quad", or "isoline"
			[UNITY_outputcontrolpoints(3)]  //  每个面片输出的顶点为3个
			[UNITY_outputtopology("triangle_cw")]  //  当创建三角形时应是顺时针还是逆时针,这里应是顺时针  "point", "line", "triangle_cw", or "triangle_ccw"
			[UNITY_partitioning("integer")]  //  如何细分切割面片   "integer", "pow2", "fractional_even", or "fractional_odd"
			[UNITY_patchconstantfunc("patchConstantFunction")]  //  细分切割部分还必须提供函数处理,每个面片调用一次
			MeshData hull (InputPatch<MeshData, 3> patch, uint id : SV_OutputControlPointID)  //  每个顶点调用一次,如果是处理三角形就是调用三次
			{
				//  如果_TessellationUniform输入值为1,不产生任何变化,但当输入值为2时,具体发生了什么
				//  对于一个三角形(因为我们配置的是处理三角形),根据三条边的二等分位置添加额外的一个顶点,如果是3就是三等分位置添加两个顶点
				//  对于一个三角形(因为我们配置的是处理三角形),根据三条个顶点添加一个中心顶点
				//  根据patchConstantFunction赋值分配生成顶点的插值权重
				return patch[id];
			}

			//  barycentricCoordinates分别代表各个点的插值权重,每个面片调用一次,对细分后的三角顶点形进行处理(也就是说原顶点不经过此处理?)
			[UNITY_domain("tri")]  //  正在处理三角形
			MeshData domain(TessellationFactors factors, OutputPatch<MeshData, 3> patch, float3 barycentricCoordinates : SV_DomainLocation)  
			{
				MeshData data;  //  新的插值权重顶点
				
				#define MY_DOMAIN_PROGRAM_INTERPOLATE(fieldName) data.fieldName = \
				patch[0].fieldName * barycentricCoordinates.x + \
				patch[1].fieldName * barycentricCoordinates.y + \
				patch[2].fieldName * barycentricCoordinates.z;

				//对所有信息利用插值权重进行插值计算
				MY_DOMAIN_PROGRAM_INTERPOLATE(vertex)
				MY_DOMAIN_PROGRAM_INTERPOLATE(normalOS)
				MY_DOMAIN_PROGRAM_INTERPOLATE(tangentOS)
				MY_DOMAIN_PROGRAM_INTERPOLATE(uv1)
				MY_DOMAIN_PROGRAM_INTERPOLATE(uv2)
				MY_DOMAIN_PROGRAM_INTERPOLATE(vertexColor)
				
				return data;
			}

			//最大的顶点数,这里比如你要生成三个三角形面片,那么一个面片需要三个顶点,就是9个顶点,一般来讲这里直接填多一点即可,不过可能填太多了会导致内存占用?
            [maxvertexcount(9)]
            void geom (triangle MeshData input[3],inout TriangleStream<VOutData> triStream)
            {
            	//原样转换过去
            	VOutData output[3];
            	for(int i=0;i<3;i++)
                {
                    VOutData p0;
                    p0 = FillBaseV2FData(input[i]);
                    output[i] = p0;
                }

				triStream.RestartStrip(); //  重新开始一个新的三角形
				triStream.Append(output[0]);
                triStream.Append(output[1]);
                triStream.Append(output[2]);

				return;

				//验证准确性用  勿删
				MeshData centerMeshData;
				centerMeshData.vertex = (input[0].vertex + input[1].vertex + input[2].vertex)/3.0;
				centerMeshData.uv1 = (input[0].uv1 + input[1].uv1 + input[2].uv1)/3.0;
				centerMeshData.uv2 = (input[0].uv2 + input[1].uv2 + input[2].uv2)/3.0;
				centerMeshData.tangentOS = (input[0].tangentOS + input[1].tangentOS + input[2].tangentOS)/3.0;
				centerMeshData.normalOS = (input[0].normalOS + input[1].normalOS + input[2].normalOS)/3.0;
				centerMeshData.vertexColor = (input[0].vertexColor + input[1].vertexColor + input[2].vertexColor)/3.0;
				centerMeshData.vertex += float4((centerMeshData.normalOS * 0.35), 0);
				VOutData center = FillBaseV2FData(centerMeshData);

				//  根据这三个点分别和中心点制造三角形输出
                triStream.RestartStrip(); //  重新开始一个新的三角形
                triStream.Append(output[1]);
                triStream.Append(center);
                triStream.Append(output[0]);

                triStream.RestartStrip();  //  重新开始一个新的三角形
                triStream.Append(output[2]);
                triStream.Append(center);
                triStream.Append(output[1]);
                
                triStream.RestartStrip(); //  重新开始一个新的三角形
                triStream.Append(output[0]);
                triStream.Append(center);
                triStream.Append(output[2]);
            }

			float4 _Color;
			
			float4 frag (VOutData i) : SV_Target
			{
				return _Color;
			}
			ENDCG
        }
    }
}

依赖文件CommonCgInc.cginc:

#ifndef CommonCgInc
#define CommonCgInc

float3 FromScript_LocalPositionWS;
float3 FromScript_LocalRotationWS;
float3 FromScript_LocalScaleWS;

//输入结构
struct MeshData
{
    float4 vertex : POSITION;
    float2 uv1 : TEXCOORD0;
    float2 uv2 : TEXCOORD1;
    float4 tangentOS :TANGENT;
    float3 normalOS : NORMAL;
    float4 vertexColor : COLOR;
};

//传递结构
struct VOutData
{
    float4 pos : SV_POSITION; // 必须命名为pos ,因为 TRANSFER_VERTEX_TO_FRAGMENT 是这么命名的,为了正确地获取到Shadow
    float2 uv1 : TEXCOORD0;
    float3 tangentWS : TEXCOORD1;
    float3 bitangentWS : TEXCOORD2;
    float3 normalWS : TEXCOORD3;
    float3 posWS : TEXCOORD4;
    float3 posOS : TEXCOORD5;
    float3 normalOS : TEXCOORD6;
    float4 vertexColor : TEXCOORD7;
    float2 uv2 : TEXCOORD8;
};

//传递结构赋值
VOutData FillBaseV2FData(MeshData input)
{
    VOutData output;
    output.pos = UnityObjectToClipPos(input.vertex);
    output.uv1 = input.uv1;
    output.uv2 = input.uv2;
    output.normalWS = normalize(UnityObjectToWorldNormal(input.normalOS));
    output.posWS = mul(unity_ObjectToWorld, input.vertex);
    output.posOS = input.vertex.xyz;
    output.tangentWS = normalize(UnityObjectToWorldDir(input.tangentOS));
    output.bitangentWS = cross(output.normalWS, output.tangentWS) * input.tangentOS.w; //乘上input.tangentOS.w 是unity引擎的bug,有的模型是 1 有的模型是 -1,必须这么写
    output.normalOS = input.normalOS;
    output.vertexColor = input.vertexColor;
    return output;
}

// Hue, Saturation, Value
// Ranges:
//  Hue [0.0, 1.0]
//  Sat [0.0, 1.0]
//  Lum [0.0, HALF_MAX]
// //HSV色彩模型转为RGB色彩模型  FROM::color.hlsl
float3 HSV2RGB( float3 hsv ){
    const float4 K = float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
    float3 p = abs(frac(hsv.xxx + K.xyz) * 6.0 - K.www);
    return hsv.z * lerp(K.xxx, saturate(p - K.xxx), hsv.y);
}
//RGB色彩模型转为HSV色彩模型  FROM::color.hlsl
float3 RGB2HSV(float3 rgb)
{
    float4 K = float4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
    float4 p = lerp(float4(rgb.bg, K.wz), float4(rgb.gb, K.xy), step(rgb.b, rgb.g));
    float4 q = lerp(float4(p.xyw, rgb.r), float4(rgb.r, p.yzx), step(p.x, rgb.r));

    float d = q.x - min(q.w, q.y);
    float e = 1.0e-10;
    return float3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}

//Gooch光照模型  ⻛格化的着⾊模型  强调冷暖色调  FROM::render4th P146
float3 GoochModel(float3 baseColor, float3 highLightColor, float3 normalWs, float3 lightDirWs, float3 viewDirWs){
    normalWs = normalize(normalWs);
    lightDirWs = normalize(lightDirWs);

    float3 coolColor = float3(0,0,0.55) + 0.25*baseColor;
    float3 warmColor = float3(0.3,0.3,0) + 0.25*baseColor;
                
    float size = clamp(100*(dot(reflect(lightDirWs, normalWs), viewDirWs) - 97), 0, 1);
    float4 halfLambert = dot(normalWs, lightDirWs) * 0.5 + 0.5;
    return  size*highLightColor + (1-size)*(halfLambert*warmColor + (1 - halfLambert)* coolColor);
}

//添加虚拟点光源  _VirtualLightFade越大,衰减越快
float3 CalculatePointVirtualLight(float3 _VirtualLightPos, float3 positionOS, float _VirtualLightFade, float3 _VirtualLightColor)
{
    float virtualLightDis = distance(_VirtualLightPos,positionOS);
    float3 virtualLight = exp(-_VirtualLightFade*virtualLightDis)*_VirtualLightColor;
    //TODO:也许补充方向衰减
    return virtualLight;
}

//切线空间计算视差uv 根据视角方向以深度反追命中点uv
float2 CalculateRealUVAfterDepth(float2 originUV, float3 viewDirTS, float depth)
{
    //计算视角方向和深度方向的cos值
    float cosTheta = dot(normalize(viewDirTS), float3(0,0,-1));  //  一般来讲unity是左手坐标系,但在切线和观察空间较为特殊是右手坐标系,不过这并不影响z轴方向的判断
    //根据深度差算出两点间距离
    float dis = depth / cosTheta;
    //算出应用深度差后对应点位
    float3 originUVPoint = float3(originUV, 0);
    float3 afrerDepthUVPoint = originUVPoint + normalize(viewDirTS) * dis;
    //返回应用深度差后对应UV
    return afrerDepthUVPoint.xy;
}
#endif

依赖文件CustomTessellation.cginc:

// Tessellation programs based on this article by Catlike Coding:
// https://catlikecoding.com/unity/tutorials/advanced-rendering/tessellation/
// 关于参数的详细说明
// https://zhuanlan.zhihu.com/p/479792793

#include  "Assets/TA/ShaderLib/CgincInclude//CommonCgInc.cginc"

//细分切割函数对于每个面片调用一次,对于每一个面片,比如三角形:每条边需要一个切割因子,内部需要一个切割因子
struct TessellationFactors {
	float edge[3] : SV_TessFactor;  //  对应三条边的切割因子
	float inside : SV_InsideTessFactor;  //  对应内部的切割因子
};

float4 _TessellationUniform;  //  细分因子,当值为1时不发生细分切割。因子可以分别设置,比如边因子是1内部因子是5,那就不会在边上生成顶点,会在中心生成五个顶点
//自定义的细分切割方法,每个面片调用一次
TessellationFactors patchConstantFunction (InputPatch<MeshData, 3> patch)
{
	TessellationFactors f;
	f.edge[0] = _TessellationUniform.x;
	f.edge[1] = _TessellationUniform.y;
	f.edge[2] = _TessellationUniform.z;
	f.inside = _TessellationUniform.w;
	return f;
}

其中,通过

#pragma hull hull
#pragma domain domain

定义hull和domain着色器,其中hull负责切割,domain用于根据权重处理细分后的顶点数据。

hull部分:

//细分切割函数对于每个面片调用一次,对于每一个面片,比如三角形:每条边需要一个切割因子,内部需要一个切割因子
struct TessellationFactors {
	float edge[3] : SV_TessFactor;  //  对应三条边的切割因子
	float inside : SV_InsideTessFactor;  //  对应内部的切割因子
};

float4 _TessellationUniform;  //  细分因子,当值为1时不发生细分切割。因子可以分别设置,比如边因子是1内部因子是5,那就不会在边上生成顶点,会在中心生成五个顶点
//自定义的细分切割方法,每个面片调用一次
TessellationFactors patchConstantFunction (InputPatch<MeshData, 3> patch)
{
	TessellationFactors f;
	f.edge[0] = _TessellationUniform.x;
	f.edge[1] = _TessellationUniform.y;
	f.edge[2] = _TessellationUniform.z;
	f.inside = _TessellationUniform.w;
	return f;
}

//如果不正确配置会报错
			[UNITY_domain("tri")]  //  正在处理三角形   "tri", "quad", or "isoline"
			[UNITY_outputcontrolpoints(3)]  //  每个面片输出的顶点为3个
			[UNITY_outputtopology("triangle_cw")]  //  当创建三角形时应是顺时针还是逆时针,这里应是顺时针  "point", "line", "triangle_cw", or "triangle_ccw"
			[UNITY_partitioning("integer")]  //  如何细分切割面片   "integer", "pow2", "fractional_even", or "fractional_odd"
			[UNITY_patchconstantfunc("patchConstantFunction")]  //  细分切割部分还必须提供函数处理,每个面片调用一次
			MeshData hull (InputPatch<MeshData, 3> patch, uint id : SV_OutputControlPointID)  //  每个顶点调用一次,如果是处理三角形就是调用三次
			{
				//  如果_TessellationUniform输入值为1,不产生任何变化,但当输入值为2时,具体发生了什么
				//  对于一个三角形(因为我们配置的是处理三角形),根据三条边的二等分位置添加额外的一个顶点,如果是3就是三等分位置添加两个顶点
				//  对于一个三角形(因为我们配置的是处理三角形),根据三条个顶点添加一个中心顶点
				//  根据patchConstantFunction赋值分配生成顶点的插值权重
				return patch[id];
			}

这里做的操作比较少,只是传入每个面片的切割因子,一个面片(三角形)的每条边需要一个切割因子,内部需要一个切割因子,一共四个切割因子。

domain部分:

			//  barycentricCoordinates分别代表各个点的插值权重,每个面片调用一次,对细分后的三角顶点形进行处理(也就是说原顶点不经过此处理?)
			[UNITY_domain("tri")]  //  正在处理三角形
			MeshData domain(TessellationFactors factors, OutputPatch<MeshData, 3> patch, float3 barycentricCoordinates : SV_DomainLocation)  
			{
				MeshData data;  //  新的插值权重顶点
				
				#define MY_DOMAIN_PROGRAM_INTERPOLATE(fieldName) data.fieldName = \
				patch[0].fieldName * barycentricCoordinates.x + \
				patch[1].fieldName * barycentricCoordinates.y + \
				patch[2].fieldName * barycentricCoordinates.z;

				//对所有信息利用插值权重进行插值计算
				MY_DOMAIN_PROGRAM_INTERPOLATE(vertex)
				MY_DOMAIN_PROGRAM_INTERPOLATE(normalOS)
				MY_DOMAIN_PROGRAM_INTERPOLATE(tangentOS)
				MY_DOMAIN_PROGRAM_INTERPOLATE(uv1)
				MY_DOMAIN_PROGRAM_INTERPOLATE(uv2)
				MY_DOMAIN_PROGRAM_INTERPOLATE(vertexColor)
				
				return data;
			}

这里则是根据传入的边和内部的切割因子的权重,对于新增加的顶点,使用这个方法中的规则填充顶点的对应信息,以完成切割。

MY_DOMAIN_PROGRAM_INTERPOLATE

是定义了一个宏用于重复处理所有的属性,还是要根据实际情况对应处理方法。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Linux进阶之路】HTTP协议

文章目录 一、基本概念1.HTTP2.域名3.默认端口号4.URL 二、请求与响应1.抓包工具2.基本框架3.简易实现3.1 HttpServer3.2 HttpRequest3.2.1 version13.2.2 version23.2.3 version3 总结尾序 一、基本概念 常见的应用层协议&#xff1a; HTTPS (HyperText Transfer Protocol Sec…

sqllab第五关通关笔记

知识点&#xff1a; 报错注入函数语法&#xff08;详见第二关笔记&#xff09;报错注入打印位数最多32位对于大于32位的数据最好使用截取函数进行控制&#xff1b;以保证输出完整mysql表中的重点数据库 information_schema &#xff08;mysql 5.0以上&#xff09; schemata …

采购管理系统:寻源到付款 (S2P) 流程自动化有什么好处?

企业的采购部门由各种流程和团队驱动&#xff0c;包括采购和应付账款。为实现战略目标而采用的策略流程之一是寻源到付款&#xff08;S2P&#xff09;流程。 何时使用 “寻源到付款”&#xff1f; 顾名思义&#xff0c;寻源到付款的主要目的是寻找最佳供应商以满足业务需求&a…

双场板功率型GaN HEMT中用于精确开关行为的电容建模

来源:Capacitance Modeling in Dual Field-Plate Power GaN HEMT for Accurate Switching Behavior (TED 16年) 摘要 本文提出了一种基于表面电势的紧凑模型&#xff0c;用于描述具有栅极和源极场板&#xff08;FP&#xff09;结构的AlGaN/GaN高电子迁移率晶体管&#xff08;…

5.BOM-操作浏览器(BOM、插件、本地存储)

BOM // BOM操作&#xff1a;操作浏览器(通过js的方式实现浏览器中的某些功能)// a)通过js的方式实现页面刷新效果// b)通过js的方式&#xff0c;实现浏览器的上一页、下一页// c)通过js的方式&#xff0c;实现页面的跳转Window对象 window是浏览器对象&#xff0c;又称为顶级对…

redis题库详解

1 什么是Redis Redis(Remote Dictionary Server) 是一个使用 C 语言编写的&#xff0c;开源的&#xff08;BSD许可&#xff09;高性能非关系型&#xff08;NoSQL&#xff09;的键值对数据库。 Redis 可以存储键和五种不同类型的值之间的映射。键的类型只能为字符串&#xff0c;…

C++函数 加括号与不加括号

很多时候&#xff0c;我们会看到一些在创建对象时有的加括号有的不加括号 那么&#xff0c;这是什么情况呢&#xff1f; 总结&#xff1a;函数需要加上括号&#xff0c;加上括号会对函数初始化&#xff0c;不加括号可能导致未知错误 我们来验证一下。 1.基本数据类型不带括…

二级指针作为函数参数——可以改变调用函数中传入指针的值(不是指向地址的值哦!)

主要是看这篇文章&#xff1a; 二级指针作为函数参数_二级指针做函数参数-CSDN博客 对里面的程序进行一些修改和补充&#xff0c;调试加更多说明。 1、一级指针情况&#xff1a; #include"stdio.h"int my_strlen1(const char* str) {int count 0;int i 0;if (N…

【功能大全】手机短信验证码一键注册登录流程

目录 发送验证码 注册登录 用户表设计 ​编辑申请腾讯云短信与密钥 找到云短信服务 开通腾讯云短信服务 ​编辑​​​​​创建短信签名 ​编辑​编辑创建短信正文模版​编辑​编辑 等待审核 测试短信​编辑 SDK密钥创建 SpringBoot集成腾讯云短信 pom中导入腾讯云短…

Uni-app跟学笔记(一):新建项目、运行、tabbar、全局配置

文章目录 1&#xff09;新建项目2&#xff09;项目运行3&#xff09;项目结构4&#xff09;开发规范5&#xff09;globalStyle全局外观配置6&#xff09;pages页面配置7&#xff09;tabbar8&#xff09;Condition 本博客为 uni-app 此门课的跟学笔记&#xff0c;目的是便于个人…

HTML5:七天学会基础动画网页12

“书接上回”继续对transition补充&#xff0c;在检查中找到ease后&#xff0c;鼠标放到ease前的紫色小方块就可以对运动曲线进行调整&#xff0c;这个曲线叫贝塞尔曲线&#xff0c;这里不做别的补充&#xff0c;不用了解&#xff0c;我们只要知道这个运动方式不只是有简单的匀…

定时执行专家 —— 让工作更高效,生活更便捷

在现代社会&#xff0c;高效的时间管理已经成为我们工作和生活中不可或缺的一部分。为了实现这一目标&#xff0c;我们经常会借助各种工具和软件来辅助我们完成定时任务。今天&#xff0c;我要为大家介绍一款功能强大、操作简便的定时任务执行软件——《定时执行专家》。这款软…

ChromeDriver 122 版本为例 国内下载地址及安装教程

ChromeDriver 国内下载地址 https://chromedriver.com/download 靠谱 千千万万别下载错了 先确认 Chrome 浏览器版本 以 win64 版本为例 那我们下载这一个啊&#xff0c;不要下载错了 下载地址贴在这哈 https://storage.googleapis.com/chrome-for-testing-public/122.0.…

差分----外部执行

概念&#xff1a; 统计学中的差分是指离散函数后的后一项减去前一项的差&#xff1b; 一维数据 输入一个长度为n的整数序列。 接下来输入m个操作&#xff0c;每个操作包含三个整数l, r, c&#xff0c;表示将序列中[l, r]之间的每个数加上c。 分析&#xff1a; 对l位置上的…

用miniconda建立PyTorch、Keras、TensorFlow三个环境

一、配置清华镜像conda源 由于网络问题&#xff0c;直接使用conda默认的源下载包可能会非常慢。为了解决这个问题&#xff0c;可以配置国内镜像源来加速包的下载。清华大学TUNA协会提供了一个常用的conda镜像源。下面是如何配置清华镜像源的步骤&#xff1a; 1. 配置清华conda…

Day37:安全开发-JavaEE应用JNDI注入RMI服务LDAP服务JDK绕过调用链类

目录 JNDI注入-RMI&LDAP服务 JNDI远程调用-JNDI-Injection JNDI远程调用-marshalsec JNDI-Injection & marshalsec 实现原理 JNDI注入-FastJson漏洞结合 JNDI注入-JDK高版本注入绕过 思维导图 Java知识点&#xff1a; 功能&#xff1a;数据库操作&#xff0c;文…

如何理解Redis中的缓存雪崩,缓存穿透,缓存击穿?

目录 一、缓存雪崩 1.1 解决缓存雪崩问题 二、缓存穿透 2.1 解决缓存穿透 三、缓存击穿 3.1 解决缓存击穿 3.2 如何保证数据一致性问题&#xff1f; 一、缓存雪崩 缓存雪崩是指短时间内&#xff0c;有大量缓存同时过期&#xff0c;导致大量的请求直接查询数据库&#xf…

idea:忽略不要搜索unpackage文件夹

开发vue时搜索关键字&#xff0c;会搜索到编译后的文件&#xff0c;如unpackage。&#xff08;注意这个是idea工具&#xff0c;和Git忽略是有区别的&#xff09; File->Settings->Editor->File Types

Ubuntu 安装腾讯会议

1.官网下载 进入腾讯会议下载官网下载腾讯会议Linux客户端 选择x86_64格式安装包下载 若不知道自己的系统架构,输入 uname -a 在命令行结果中查看系统架构信息 2.终端命令安装 cd {你的下载路径} sudo dpkg -i TencentMeeting_0300000000_3.19.0.401_x86_64_default.publi…

2024-03-11,12(HTML,CSS)

1.HTML的作用就是在浏览器摆放内容。 2.HTML基本骨架 head&#xff1a;网页头部&#xff0c;是给浏览器看的代码&#xff0c;例如CSS body&#xff1a;网页主体&#xff0c;是给用户看的代码&#xff0c;例如图片&#xff0c;文字。 title&#xff1a;网页标题 3.标签的两种…