OpenGL ES 入門之旅--灰度,旋渦,馬賽克濾鏡

原圖.jpg

前情提要

這篇濾鏡效果的實現(xiàn)是在上一篇分屏濾鏡的基礎(chǔ)上來進行實現(xiàn)的,同樣的前提是可以利用GLSL加載一張正常的圖片。

思路.png
詳情請參考上一篇OpenGL ES 入門之旅--分屏濾鏡
下面步入這篇的正題:

灰度濾鏡

一張圖片的顯示是由三個顏色通道(RGB)來決定的,所以圖片也稱為三通道圖。

三通道圖:圖片每個像素點都有三個值表示 ,所以就是三通道。也有四通道的圖。例如RGB圖片即為三通道圖片,RGB色彩模式是工業(yè)界的一種顏色標準,是通過對紅(R)、綠(G)、藍(B)三個顏色通道的變化以及它們相互之間的疊加來得到各式各樣的顏色的,RGB即是代表紅、綠、藍三個通道的顏色,這個標準幾乎包括了人類視力所能感知的所有顏色,是目前運用最廣的顏色系統(tǒng)之一??傊?,每一個點由三個值表示。

理解了三通道圖的概念,那么灰度濾鏡其實就是只有一個通道有值,也就是只要得到圖片的亮度即可,其實這也就是單通道圖。

單通道圖:俗稱灰度圖,每個像素點只能有有一個值表示顏色,它的像素值在0到255之間,0是黑色,255是白色,中間值是一些不同等級的灰色。(也有3通道的灰度圖,3通道灰度圖只有一個通道有值,其他兩個通道的值都是零)。

有5中方法來實現(xiàn)灰度濾鏡的算法(前三種方法是利用權(quán)重來實現(xiàn)的):

  • 浮點算法: Gray = R * 0.3 + G * 0.59 + B * 0.11 (根據(jù)對應(yīng)紋素的顏色值調(diào)整RGB的比例)
  • 整數(shù)算法: Gray = (R * 30 + G * 59 + B * 11) / 100 (同浮點算法)
  • 移位算法: Gray = (R * 76 + G * 151 + B * 28) >> 8
  • 平均值法: Gray = (R + G + B) / 3; (獲取到對應(yīng)紋素的RGB平均值,填充到三個通道上面)
  • 僅取綠色: Gray = G (一個顏色填充三個通道)

同樣的,灰度濾鏡只需要更改片元著色器的代碼即可:
片元著色器代碼:

precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;
const highp vec3 W = vec3(0.2125, 0.7154, 0.0721);

void main (void) {
    
    vec4 mask = texture2D(Texture, TextureCoordsVarying);
    float luminance = dot(mask.rgb, W);
    gl_FragColor = vec4(vec3(luminance), 1.0);
}

實現(xiàn)效果:

灰度.png

顛倒濾鏡

其實對于顛倒濾鏡,既可以在頂點著色器中修改,也可以在片元著色器中修改,但是在頂點著色器修改的好處是只需要顛倒頂點,計算量相比較少。

首先來修改頂點著色器的代碼看下
頂點著色器代碼:

attribute vec4 Position;
attribute vec2 TextureCoords;
varying vec2 TextureCoordsVarying;

void main (void) {
    gl_Position = vec4(Position.x,  - Position.y, 0.0, 1.0);
    TextureCoordsVarying = TextureCoords;
}

或者修改片元著色器的代碼
片元著色器代碼:

precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;

void main (void) {
    vec4 mask = texture2D(Texture, vec2(TextureCoordsVarying.x, 1.0 - TextureCoordsVarying.y));
    gl_FragColor = vec4(mask.rgb, 1.0);
}

實現(xiàn)效果:

顛倒.png

這里有個問題需要注意一下,在GLSL渲染圖片時,原本圖片就是倒置的,只不過在獲取紋理的代碼中,將圖片進行了翻轉(zhuǎn)。

CGContextRef context = CGBitmapContextCreate(imageData, width, height, 8, width * 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
//將圖片翻轉(zhuǎn)過來(圖片默認是倒置的)
CGContextTranslateCTM(context, 0, height);
CGContextScaleCTM(context, 1.0f, -1.0f);
CGColorSpaceRelease(colorSpace);
CGContextClearRect(context, rect);

可能會有同學(xué)覺得,只要在此處不把圖片翻轉(zhuǎn),就一樣可以實現(xiàn)顛倒濾鏡,實際上此法并不可取,我們在此處說的顛倒濾鏡只是一種濾鏡效果,當(dāng)切換到別的濾鏡就是另外一種效果了,如果不解決圖片的翻轉(zhuǎn)問題,就會導(dǎo)致別的濾鏡效果是倒置的了。(個人理解,還請指教)。

旋渦濾鏡

旋渦濾鏡的主要原理:圖像的漩渦主要是在某個半徑范圍里,把當(dāng)前采樣點旋轉(zhuǎn) ?定?度,旋轉(zhuǎn)以后當(dāng)前點的顏?就被旋轉(zhuǎn)后的點的顏?代替,因此整個半徑范圍?會有旋轉(zhuǎn)的效果。如果旋轉(zhuǎn)的時候旋轉(zhuǎn)?度隨著當(dāng)前點離半徑的距離遞減,整個 圖像就會出現(xiàn)漩渦效果。這?使用的了了拋物線遞減因子:(1.0-(r/Radius)*(r/Radius))

旋渦濾鏡相比較灰度濾鏡,顛倒濾鏡會有些復(fù)雜,所以先來看一下實現(xiàn)的效果,然后再來分析是怎么實現(xiàn)的,
片元著色器代碼:

//著色器代碼中不要加中文注釋,否則可能報錯,此處只是為了理解,特別做的注釋。
precision mediump float; //PI
const float PI = 3.14159265; 
//紋理采樣器
uniform sampler2D Texture; 
//旋轉(zhuǎn)角度
const float uD = 60.0; 
//旋渦系數(shù)
const float uR = 0.5;
//紋理坐標
varying vec2 TextureCoordsVarying;
void main() {
//旋轉(zhuǎn)正方形范圍:[512,512]
ivec2 ires = ivec2(512, 512); //獲取旋轉(zhuǎn)的直徑
float Res = float(ires.s); //紋理坐標[0,0],[1,0],[0,1],[1,1]
vec2 st = TextureCoordsVarying;
//半徑 = 直徑 * 0.5;
float Radius = Res * uR;
//準備旋轉(zhuǎn)處理的紋理坐標 = 直徑  * 紋理坐標
vec2 xy = Res * st;
//紋理坐標的?半
vec2 dxy = xy - vec2(Res/2., Res/2.);
//r
float r = length(dxy);
//拋物線遞減因子:(1.0-(r/Radius)*(r/Radius) )
float beta = atan(dxy.y, dxy.x) + radians(uD) * 2.0 * (1.0-(r/Radius)*(r/Radius));
if(r<=Radius)
{
//獲取的紋理坐標旋轉(zhuǎn)beta度.
xy = Res/2.0 + r*vec2(cos(beta), sin(beta));
}
//st = 旋轉(zhuǎn)后的紋理坐標/旋轉(zhuǎn)范圍 
st = xy/Res;
//將旋轉(zhuǎn)的紋理坐標替換原始紋理坐標TextureCoordsVarying 獲取對應(yīng)像素點的顏色. 
vec3 irgb = texture2D(Texture, st).rgb;
//將計算后的顏?填充到像素點中 gl_FragColor
gl_FragColor = vec4( irgb, 1.0 );
}

實現(xiàn)效果:

旋渦.png

個人理解(如果以一張帶著紋理坐標的圖片來理解):
對于這兩句代碼

ivec2 ires = ivec2(512, 512); //獲取旋轉(zhuǎn)的直徑
float Res = float(ires.s); //紋理坐標[0,0],[1,0],[0,1],[1,1]

這里的ivec2(512, 512);這個坐標范圍其實只要大于1就可以了,這個數(shù)值可以寫大于1的任何數(shù)值。Res得到的結(jié)果其實就是紋理坐標的最大值1,這里我們把它理解為要旋轉(zhuǎn)的直徑 。而float Radius = Res * uR;表示以紋理坐標中心點(0.5,0.5)的旋渦半徑。如果你想控制圓形漩渦的半徑或者圓形漩渦的位置,uR控制著圓的半徑,向量vec2(Res/2.0, Res/2.0)控制著圓心位置

dxy.png
假設(shè)在紋理中取任意一點紋素,那么它的坐標就是vec2 xy = 1 * st,然后根據(jù)平行四邊形法則,將該紋素的坐標與紋理的中心點坐標相減,得到向量dxy,vec2 dxy = xy - vec2(1.0/2.0, 1.0/2.0),這樣就形成了一個正三角,然后對向量dxy取模,float r = length(dxy);得到長度r,這個r值就是在下面判斷是否在旋渦半徑范圍內(nèi)。

下面來看一下,該紋素的當(dāng)前角度:

當(dāng)前角度.png

當(dāng)前角度:tanθ = dxy.y / dxy.x,那么θ = atan(dxy.y, dxy.x)

下面再來看一下旋渦角度的獲取,

beta.png

我們在開始的使用已經(jīng)寫定了旋渦的角度為uD,那么加劇旋渦角度為atan(dxy.y, dxy.x) + radians(uD) * 2.0,而紋素距離圓心的距離不同旋渦程度也不同,所以在此處引入衰減因子(1.0-(r/Radius)*(r/Radius) ), 所以得到加劇漩渦衰減角度為float beta = atan(dxy.y, dxy.x) + radians(uD) * 2.0 * (1.0-(r/Radius)*(r/Radius));

得到旋渦角度之后,假設(shè)旋渦前的點為(x,y),旋渦之后的點為(x1,y1),旋渦角度為beta,向量dxy的模r是不變的,那么就可以得到一個新向量dx1y1 = r * vec2(cos(beta), sin(beta)),根據(jù)向量加法原則,vec2(Res/2.0, Res/2.0) + r*vec2(cos(beta), sin(beta))就是經(jīng)過旋渦之后的最終向量(x1,y1),然后把它還原成紋理坐標st = xy/Res;也就是經(jīng)過旋渦之后新的紋素點的坐標。 然后再將旋渦后的紋理坐標替換原始紋理坐標vec3 irgb = texture2D(Texture, st).rgb;,然后再將計算后的顏色填充到像素點中。

旋渦效果的實現(xiàn)大概就是這樣(個人理解,如有錯誤,還請指正)。

馬賽克濾鏡

?賽克效果就是把圖片的?個相當(dāng)?小的區(qū)域?同一個點的顏色來表示.可以認為是大規(guī)模的降低圖像的分辨率,而讓圖像的一些細節(jié)隱藏起來。
馬賽克單元格.png

也就是說,根據(jù)馬賽克單元格的寬高計算出圖像總的馬賽克行數(shù)和列數(shù),然后將每個馬賽克單元格遍歷2次,第一次計算該單元格RGB的平均值,第二次遍歷賦顏色值。

片元著色器代碼

precision mediump float;
//紋理坐標
varying vec2 TextureCoordsVarying;
//紋理采樣器
uniform sampler2D Texture;
//紋理圖片size
const vec2 TexSize = vec2(600.0, 600.0);
//馬賽克size
const vec2 mosaicSize = vec2(16.0, 16.0);

void main()
{
    //計算圖像的實際位置
    vec2 intXY = vec2(TextureCoordsVarying.x*TexSize.x, TextureCoordsVarying.y*TexSize.y);
    // floor (x) 內(nèi)建函數(shù),返回小于/等于X的最大整數(shù)值.
    // floor (intXY.x / mosaicSize.x) * mosaicSize.x 計算出一個?小?賽克的坐標.
    vec2 XYMosaic = vec2(floor(intXY.x/mosaicSize.x)*mosaicSize.x, floor(intXY.y/mosaicSize.y)*mosaicSize.y);
    //換算回紋理坐標
    vec2 UVMosaic = vec2(XYMosaic.x/TexSize.x, XYMosaic.y/TexSize.y);
    //獲取到馬賽克后的紋理坐標的顏色值
    vec4 color = texture2D(Texture, UVMosaic);
    //將?賽克顏色值賦值給gl_FragColor. 
    gl_FragColor = color;
}

實現(xiàn)效果:

馬賽克.png

六邊形馬賽克

首先看看六邊形馬賽克的結(jié)構(gòu),如下圖:

六邊形馬賽克結(jié)果.png

下面先來看下片元著色器代碼和實現(xiàn)效果,之后再來研究下原理
片元著色器代碼

precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;

const float mosaicSize = 0.03;

void main (void)
{
    float length = mosaicSize;
    float TR = 0.866025;
    
    float x = TextureCoordsVarying.x;
    float y = TextureCoordsVarying.y;
    
    int wx = int(x / 1.5 / length);
    int wy = int(y / TR / length);
    vec2 v1, v2, vn;
    
    if (wx/2 * 2 == wx) {
        if (wy/2 * 2 == wy) {
            //(0,0),(1,1)
            v1 = vec2(length * 1.5 * float(wx), length * TR * float(wy));
            v2 = vec2(length * 1.5 * float(wx + 1), length * TR * float(wy + 1));
        } else {
            //(0,1),(1,0)
            v1 = vec2(length * 1.5 * float(wx), length * TR * float(wy + 1));
            v2 = vec2(length * 1.5 * float(wx + 1), length * TR * float(wy));
        }
    }else {
        if (wy/2 * 2 == wy) {
            //(0,1),(1,0)
            v1 = vec2(length * 1.5 * float(wx), length * TR * float(wy + 1));
            v2 = vec2(length * 1.5 * float(wx + 1), length * TR * float(wy));
        } else {
            //(0,0),(1,1)
            v1 = vec2(length * 1.5 * float(wx), length * TR * float(wy));
            v2 = vec2(length * 1.5 * float(wx + 1), length * TR * float(wy + 1));
        }
    }
    
    float s1 = sqrt(pow(v1.x - x, 2.0) + pow(v1.y - y, 2.0));
    float s2 = sqrt(pow(v2.x - x, 2.0) + pow(v2.y - y, 2.0));
    if (s1 < s2) {
        vn = v1;
    } else {
        vn = v2;
    }
    vec4 color = texture2D(Texture, vn);
    
    gl_FragColor = color;
    
}

實現(xiàn)效果:

六邊形馬賽克.png

思路: 我們要做的效果就是讓?張圖片,分割成由多個六邊形組成,讓每 個六邊形中的顏色相同(直接取六邊形中心點紋素RGB比較?便)

分割成六邊形.png

如上圖,畫出很多長和寬比例為 3:√3 的的矩形陣。然后我們可以 對每個點進行編號,如上圖中,采?坐標系標記.
假如我們的屏幕的左上點為上圖的(0,0)點,則屏幕上的任一點我 們找到它所對應(yīng)的那個矩形。
假定我們設(shè)定的矩陣比例為 3*LEN : √3*LEN ,那么屏幕上的任意 點(x, y)所對應(yīng)的矩陣坐標為(int(x/(3*LEN)), int(y/ (√3*LEN)))。那么(wx, wy) 表示紋理坐標在所對應(yīng)的矩陣坐標為:
wx = int(x/(1.5 * length)); wy = int(y/(TR * length))

先來看下其中一個六邊形:


屏幕快照 2019-07-05 上午9.54.33.png
一個六邊形.png

雖然一個六邊形周圍有9個點,但是是中心點的只有5個(圖中綠色的點:當(dāng)前這個六邊形的中心點和周邊四個六邊形的中心點)。
這么來看可以把一個六邊形分割成四塊區(qū)域,兩種類型:

左上點右下點為中心點(綠色點)

左上右下.png

左下點和右上點為中心點(綠色點)
左下右上.png

任一塊區(qū)域四個點的計算公式如下 :
公式.png

那么如果取任意一個點(下圖中紅色點),這個點的顏色值是由距離它最近的六邊形的中心點決定的,距離這個點最近的有兩個中心點(左上點右下點/左下點右上點),所以首先要算出這個紅點到哪個中心點距離最近
任取點.png

那么該怎么判斷這個紅點距離較近的中心點是哪兩個中心點呢?再來看一下上面的分割成六邊形.png,在劃分六邊形的時候已經(jīng)對分割的區(qū)域進行了行列標號,

  • 偶數(shù)行偶數(shù)列計算左上中心點和右下中心點
  • 偶數(shù)行奇數(shù)列計算左下中心點和右上中心點
  • 奇數(shù)行偶數(shù)列計算左下中心點和右上中心點
  • 奇數(shù)行奇數(shù)列計算左上中心點和右下中心點

計算出V1,V2(片元著色器代碼中定義的)兩點的坐標之后,再把當(dāng)前坐標Vn(片元著色器代碼中定義的)分別求出距離V1,V2的距離,然后判斷這兩個距離S1,S2的大小,然后獲取距離小的中心點的顏色值,進行顏色賦值。
(六邊形馬賽克的理解大概就是這些了,如有錯誤,還請指正)

三角形馬賽克

關(guān)于三角形馬賽克首先可以肯定的是馬賽克是等邊三角形,這樣才能真正的無縫拼接。
如果有理解了上面的六邊形馬賽克的原理,那么三角形馬賽克就很好理解了,觀察發(fā)現(xiàn),一個六邊形正好可以用六個三角形拼湊而成。

6個三角形.png

如果任意取一點(下圖中紅色的大點),該點顏色值就取它所在的三角形的中心點的顏色值,要判斷一個點屬于哪個三角形,必須先判斷它屬于那個六邊形,這個在之前的六邊形馬賽克中已經(jīng)提到了。
任一點.png
知道了該點在哪個六邊形之后,也就知道了該點的坐標,然后根據(jù)該點和六邊形中心點的夾角范圍是不是就知道了這個點位于哪個三角形內(nèi)了。

夾角的計算 float θ = atan((x-O.x)/(y-O.y));這里注意一下atan算出的范圍是-180度至180度,對應(yīng)的數(shù)值是-PIPI
根據(jù)這個角度就能知道這個點位于這六個三角形中的哪一個三角形了(在上圖中已經(jīng)對這六個三角形進行了標記劃分)。然后再計算這六個三角形各自的中心點坐標(上圖中的小紅點),任取的點屬于哪個三角形就取該三角形中心點的顏色值。最后再進行顏色賦值。

片元著色器代碼

precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;

float mosaicSize = 0.03;

void main (void){
    const float TR = 0.866025;
    const float PI6 = 0.523599;
    
    float x = TextureCoordsVarying.x;
    float y = TextureCoordsVarying.y;
    
    int wx = int(x/(1.5 * mosaicSize));
    int wy = int(y/(TR * mosaicSize));
    
    vec2 v1, v2, vn;
    
    if (wx / 2 * 2 == wx) {
        if (wy/2 * 2 == wy) {
            v1 = vec2(mosaicSize * 1.5 * float(wx), mosaicSize * TR * float(wy));
            v2 = vec2(mosaicSize * 1.5 * float(wx + 1), mosaicSize * TR * float(wy + 1));
        } else {
            v1 = vec2(mosaicSize * 1.5 * float(wx), mosaicSize * TR * float(wy + 1));
            v2 = vec2(mosaicSize * 1.5 * float(wx + 1), mosaicSize * TR * float(wy));
        }
    } else {
        if (wy/2 * 2 == wy) {
            v1 = vec2(mosaicSize * 1.5 * float(wx), mosaicSize * TR * float(wy + 1));
            v2 = vec2(mosaicSize * 1.5 * float(wx+1), mosaicSize * TR * float(wy));
        } else {
            v1 = vec2(mosaicSize * 1.5 * float(wx), mosaicSize * TR * float(wy));
            v2 = vec2(mosaicSize * 1.5 * float(wx + 1), mosaicSize * TR * float(wy+1));
        }
    }
    
    float s1 = sqrt(pow(v1.x - x, 2.0) + pow(v1.y - y, 2.0));
    float s2 = sqrt(pow(v2.x - x, 2.0) + pow(v2.y - y, 2.0));
    
    if (s1 < s2) {
        vn = v1;
    } else {
        vn = v2;
    }
    
    vec4 mid = texture2D(Texture, vn);
    float a = atan((x - vn.x)/(y - vn.y));
    
    vec2 area1 = vec2(vn.x, vn.y - mosaicSize * TR / 2.0);
    vec2 area2 = vec2(vn.x + mosaicSize / 2.0, vn.y - mosaicSize * TR / 2.0);
    vec2 area3 = vec2(vn.x + mosaicSize / 2.0, vn.y + mosaicSize * TR / 2.0);
    vec2 area4 = vec2(vn.x, vn.y + mosaicSize * TR / 2.0);
    vec2 area5 = vec2(vn.x - mosaicSize / 2.0, vn.y + mosaicSize * TR / 2.0);
    vec2 area6 = vec2(vn.x - mosaicSize / 2.0, vn.y - mosaicSize * TR / 2.0);
    
    
    if (a >= PI6 && a < PI6 * 3.0) {
        vn = area1;
    } else if (a >= PI6 * 3.0 && a < PI6 * 5.0) {
        vn = area2;
    } else if ((a >= PI6 * 5.0 && a <= PI6 * 6.0) || (a < -PI6 * 5.0 && a > -PI6 * 6.0)) {
        vn = area3;
    } else if (a < -PI6 * 3.0 && a >= -PI6 * 5.0) {
        vn = area4;
    } else if(a <= -PI6 && a> -PI6 * 3.0) {
        vn = area5;
    } else if (a > -PI6 && a < PI6) {
        vn = area6;
    }
    
    vec4 color = texture2D(Texture, vn);
    gl_FragColor = color;
}

這里,TR其實是√3/2,而PI6明顯就是PI/6,對應(yīng)的是30度。

實現(xiàn)效果:

三角形馬賽克.png

圓形馬賽克

圓形馬賽克其實和正方形馬賽克的原理差不多,在這里可能有一點疑惑,對于上面的正方形馬賽克,六邊形馬賽克,三角形馬賽克,它們都能很好的無縫拼接,而圓形是不能無縫拼接的,在這里這樣來處理,對于任取一點,判斷該點是在哪個圓形范圍內(nèi),如果在圓形范圍內(nèi),那么就取這個圓形的中心點處的顏色值,如果任取一點不在任何一個圓形范圍內(nèi),那么可以不改變這個點的顏色值,就是跟原來一樣啊,其實,如果把圓形馬賽克設(shè)置的圓很小的話,那么就近似于無縫拼接了。(在這里就不對圓形馬賽克做過度理解了,原理都類似)

片元著色器代碼

precision highp float;
uniform sampler2D Texture0;

const vec2 texSize = vec2(640., 640.);
const vec2 mosaicSize = vec2(18., 18.);

varying vec2 TextureCoordsVarying;

void main(void)
{
    vec2 xy = vec2(TextureCoordsVarying.x * texSize.x, TextureCoordsVarying.y * texSize.y);
   
    vec2 xyMosaic = vec2(floor(xy.x / mosaicSize.x) * mosaicSize.x, floor(xy.y / mosaicSize.y) * mosaicSize.y )+ .5*mosaicSize;
    vec2 delXY = xyMosaic - xy;
    float delL = length(delXY);
    
    vec2 uvMosaic = vec2(xyMosaic.x / texSize.x, xyMosaic.y / texSize.y);
    
    vec4 finalColor;
    if(delL<0.5*mosaicSize.x)
    {
        finalColor = texture2D(Texture0, uvMosaic);
    }
    else
    {
        finalColor = texture2D(Texture0, TextureCoordsVarying);
        //finalColor = vec4(0., 0., 0., 1.);
    }
    
    gl_FragColor = finalColor;
}

實現(xiàn)效果:

圓形馬賽克.png

馬賽克濾鏡到此就告一段落了,后面會繼續(xù)補充其他濾鏡功能,奈何本人水平很菜,如若筆者對上述內(nèi)容有理解錯誤的地方還請指正,多謝!

最后附上Demo地址:https://github.com/Henry-Jeannie/Mosaic

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容