在纸上涂一个灰色的圆,它可能是圆片,也可能是球。轮廓没有告诉我们全部信息。只要把圆内部的明暗组织起来,我们就可能读出一个朝向不同方向的表面。
明暗变化可以成为体积的线索;落到别处的影子,还能帮助我们判断物体与环境的关系。 从这两个联系出发,再用杯子的逐步图解区分接触、边缘与反光。不要求你会排线,先学会根据图中证据判断。
先区分:轮廓、受光与投影
看下面三个小图。A 只有轮廓和平涂;B 增加表面明暗;C 又增加地面的影子。它们分别增加了什么信息?
圆的轮廓是平面的形状。B 内部由亮到暗的变化,让我们更容易把它理解成球面。C 的投影则提示球与地面的接触和光的大致方向。轮廓、表面明暗、投影是三种线索,不是同一个东西。 静态图是关系示意,下面再用同一几何模型核对变化。
为什么同一表面也有亮有暗?
想象把一张白纸正对台灯,再缓缓倾斜。纸没有换颜色,但表面的朝向变了。对于简单的哑光表面,迎着光的部分能接到较多照明;转向侧面的部分接到较少。
球面可以想成许多朝向略有不同的小面。它们连在一起,受光也连续变化,于是出现圆润的明暗过渡。方盒子的面朝向突然改变,面与面之间就可能出现比较明显的明暗分界。
这里说的是材质相同、照明条件明确时的比较。深色物体即使受光,也未必比浅色物体的暗部亮;不能只凭整张图里哪个像素最亮,就断定光源在哪里。
动手核对:移动光源
先预测光从左边移到右边时,会发生两件什么事。再切换“加入受光”和“加入投影”,分别观察球面与地面。
正在呈现知识画面
同一个球,逐层看清明暗固定球体、地面、相机与材质。平涂只保留轮廓;受光模式按表面朝向改变灰度;投影模式再判断球体是否挡住到达地面的光。滑杆只改变远处光源的水平方向。
{"id":"light-and-volume","title":"同一个球,逐层看清明暗","summary":"固定球体、地面、相机与材质。平涂只保留轮廓;受光模式按表面朝向改变灰度;投影模式再判断球体是否挡住到达地面的光。滑杆只改变远处光源的水平方向。","height":680,"html":"<h2 data-i18n=\"heading\"></h2><p data-i18n=\"intro\"></p><canvas class=\"scene\" id=\"light-scene\" width=\"480\" height=\"320\" role=\"img\" data-i18n-label=\"scene\"></canvas><div class=\"controls\" role=\"group\" data-i18n-label=\"choices\"><button type=\"button\" data-mode=\"flat\" aria-pressed=\"false\" data-i18n=\"flat\"></button><button type=\"button\" data-mode=\"form\" aria-pressed=\"false\" data-i18n=\"form\"></button><button type=\"button\" data-mode=\"shadow\" aria-pressed=\"false\" data-i18n=\"shadow\"></button></div><label for=\"angle\" data-i18n=\"angle_label\"></label><input id=\"angle\" type=\"range\" min=\"-70\" max=\"70\" value=\"-45\" step=\"1\"><output id=\"angle-value\" for=\"angle\" class=\"readout\"></output><div class=\"status\"><p id=\"status\" role=\"status\" aria-live=\"polite\"></p></div><p class=\"readout\" data-i18n=\"note\"></p>","css":"\nbody { padding:22px; }\nh2 { font-size:20px; line-height:1.4; }\np { font-size:16px; line-height:1.65; }\n.scene { display:block; width:100%; height:auto; overflow:hidden; border-radius:4px; background:#e8e4dc; }\n.scene * { transition:none!important; }\n.controls { display:flex; flex-wrap:wrap; align-items:center; gap:8px; margin:16px 0; }\n.controls button { flex:1 1 auto; min-height:44px; font-size:16px; }\nlabel { display:block; font-size:16px; margin:14px 0 4px; }\ninput[type=range] { width:100%; min-height:28px; }\n.readout { font-size:16px; color:var(--muted); }\n.status { display:grid; border-top:1px solid var(--rule); padding-top:12px; margin-top:16px; }\n.status p { grid-area:1/1; margin:0; }\n.status [aria-hidden=true] { visibility:hidden; }\n@media(max-width:480px) { body {padding:16px 12px;} h2 {font-size:19px;} }\n","js":"const $=s=>document.querySelector(s); const $$=s=>Array.from(document.querySelectorAll(s));\nfunction say(key){$('#status').textContent=viz.t(key);document.body.dataset.state=key;}\nfunction reserve(keys){keys.forEach(k=>{const p=document.createElement('p');p.textContent=viz.t(k);p.setAttribute('aria-hidden','true');$('.status').append(p);});}\nfunction selected(attr,value){$$('button['+attr+']').forEach(b=>b.setAttribute('aria-pressed',String(b.getAttribute(attr)===value)));}\n\nconst canvas=$('#light-scene'),ctx=canvas.getContext('2d'); let mode='shadow';\nconst norm=a=>{const s=Math.hypot(...a);return a.map(v=>v/s);}, dot=(a,b)=>a.reduce((v,x,i)=>v+x*b[i],0);\nconst ray=norm([0,-.35,-1]),up=[0,-ray[2],ray[1]];\nfunction hit(o,d){const b=dot(o,d),c=dot(o,o)-1,disc=b*b-c;if(disc<0)return Infinity;const t=-b-Math.sqrt(disc);return t>1e-4?t:Infinity;}\nfunction render(){\n const angle=Number($('#angle').value),a=angle*Math.PI/180,L=norm([Math.sin(a),.8,Math.cos(a)]);\n const out=ctx.createImageData(480,320);\n for(let y=0;y<320;y++)for(let x=0;x<480;x++){\n const sx=(x-240)/91,sy=(160-y)/91,o=[sx,up[1]*sy-ray[1]*6,up[2]*sy-ray[2]*6];\n const ts=hit(o,ray),tg=(-1-o[1])/ray[1];let val=.72;\n if(ts<tg && ts!==Infinity){const p=o.map((v,i)=>v+ray[i]*ts);val=mode==='flat'?.4:.06+.60*Math.max(0,dot(p,L));}\n else if(tg>0){const p=o.map((v,i)=>v+ray[i]*tg);p[1]+=.001;const blocked=mode==='shadow'&&hit(p,L)!==Infinity;val=.11+(blocked?0:.57*L[1]);}\n const c=Math.round(255*(val<=.0031308?12.92*val:1.055*Math.pow(val,1/2.4)-.055)),i=(y*480+x)*4;\n out.data[i]=out.data[i+1]=out.data[i+2]=c;out.data[i+3]=255;\n }\n ctx.putImageData(out,0,0);\n const lx=240+L[0]*150,ly=135-dot(L,up)*130;ctx.strokeStyle='#b5790f';ctx.fillStyle='#b5790f';ctx.lineWidth=4;\n ctx.beginPath();ctx.arc(lx,ly,8,0,Math.PI*2);ctx.fill();ctx.beginPath();ctx.moveTo(lx,ly);ctx.lineTo(lx-L[0]*36,ly+dot(L,up)*36);ctx.stroke();\n selected('data-mode',mode);say('s_'+mode);$('#angle-value').textContent=viz.number(angle,0)+'°';\n $('#angle').disabled=mode==='flat';document.body.dataset.angle=angle;document.body.dataset.lightX=L[0];\n}\nreserve(['s_flat','s_form','s_shadow']);$$('button[data-mode]').forEach(b=>b.onclick=()=>{mode=b.dataset.mode;render();});$('#angle').oninput=render;render();\n","audio":false,"strings":{"angle_label":"光源左右方向","choices":"显示层次","flat":"平涂轮廓","form":"加入受光","heading":"光换方向,哪里会变?","intro":"先预测:光移到右边时,球的亮部和地面的影子分别往哪边走?再拖动滑杆核对。","note":"简化模型:远处单一方向光、理想哑光球、固定的少量环境填充。硬投影来自方向光;没有模拟高光、多次反射或窗光软影。","s_flat":"平涂保留同一个圆形轮廓。只有一个均匀灰面,提供的体积线索很少。","s_form":"朝向光源的球面更亮,转离光源的部分更暗。地面投影暂时关闭,便于分开观察。","s_shadow":"球体还会挡住本应到达地面的光。光在左侧时,投影偏右;换到右侧后,关系反过来。","scene":"固定相机下的灰球与地面,黄色标记表示光源方向。","shadow":"加入投影"}}
这个实验用理想哑光球和一个远处的方向光。它按球面朝向计算直接受光,再检查球是否遮住通往光源的路径。少量固定环境填充让背光面仍可辨认;这不是一次完整的室内光照模拟。
注意:球自身没有直接受光的部分,是物体表面的暗部;投影是它挡住光后,让另一个表面变暗的区域。 投影不是贴在球旁边的一块固定黑椭圆,换了光向,它的位置也应改变。
从一个球到一幅画:不要把所有“亮”和“暗”画成同一种东西
下面用同一个简化球体区分几种任务。它们都是结构示意,便于把位置和边缘分开看;严格随光向变化的关系仍在上面的交互中核对。
A 与 B:先看影子边缘。 较小、较远、相对角尺寸很小的光源常产生较硬的投影;具有可观角尺寸的窗户、柔光面等会形成半影。B 只柔化投影边缘,让你看清“影子在什么地方”和“它的边缘有多硬”是两项判断。真实软影还常随遮挡物与承影面距离变化,不能把整张画统一模糊就当作柔光。
C:最亮的一小点可能是高光。 高光与观看方向、光源和表面的反射方式有关;它不只是“普通亮部再加一点白”。亮部可以是一整片有方向的面,高光往往更局部。哑光陶器、高光瓷器和金属即使轮廓相同,也不能照搬同一块高光。
D:轮廓边缘与内部过渡要分开处理。 球的轮廓遮住后方背景,可以形成明确边界;球面内部朝向连续变化,明暗通常渐变。盒子跨过棱线时,面朝向突然改变,可以出现更明确的明暗分界。“圆物体的每一条边都要糊掉”“硬物体内部只能平涂”都把不同问题混在了一起。
完整示范:给一个浅色杯子建立可信的明暗
假设杯子靠近一扇左侧窗户,表面基本哑光,目标是表现“立在桌上”的体积。可以按下面的顺序判断,画图也按这个顺序推进。
四幅图的杯子、视角和左上主光方向相同。A 先定轮廓与材料底色;B 用两组明暗标出朝向的转折;C 加上向右延伸且与杯底相接的投影和接触暗部;D 才把硬分组过渡为圆柱面的渐变,并区分杯口内侧、口沿与暗侧的少量反光。
这四幅是作画顺序的示意,不是让真实光逐帧改变。B 的硬分组是暂时的概括:如果一直保留那条直界,杯身会显得折面化。D 的过渡服务杯身转面,轮廓仍然清楚;暗侧的反光没有超过主亮面。先用 B、C 检查结构,避免一开始就靠柔化掩盖接触或方向错误。
- 先确定照明与材料。 杯子本身是浅色,主光来自左侧。这两句话要同时成立;不能把右侧暗部理解成杯子突然换成了深色材料。
- 先分亮面与暗面。 在杯子的圆柱部分,沿表面从左向右寻找朝向逐渐离开光源的位置。先用少量明暗组建立转折,不急着抹出几十级过渡。
- 把接触和投影接起来。 投影应从杯子接触桌面的附近开始,方向与照明一致;若它与杯底明显脱开,读者可能把杯子看作悬浮。杯柄也会遮光,不能只给杯身贴一块无关的椭圆。
- 再处理反光与边缘。 桌面可能把光反射到杯子暗侧,因此暗部不一定一片死黑。但在这个简单场景中,反光仍应服从整体暗面,不应随意画成一个与主光亮面争抢的亮斑。
- 最后才加小变化。 杯口厚度、柄的内侧、局部高光和纹理帮助说明材料;它们不能弥补大方向错误。
这是一套检查顺序,不是一份固定灰色配方。换成黑杯子、背光或多盏灯之后,各部分的明暗范围会变,先前的机械口诀就不够了。
| 看起来哪里不对 | 先查什么 | 一次优先修改什么 |
| 物体像纸片 | 内部明暗是否随表面朝向变化 | 先建立大转折,暂缓纹理 |
| 物体像浮起来 | 接触处与投影是否连贯 | 修正接触关系和投影起点 |
| 表面像塑料或金属,但原本想画陶器 | 高光是否过强、过硬、过集中 | 先比较高光与大亮面的关系 |
| 整体发脏 | 是否到处都有小黑点和随机渐变 | 收拢亮暗组,只在结构需要处加变化 |
接下来回到观察图,分别找“表面转过去了”“别的物体挡住了光”“材料本身更暗”的证据。三者可能同时出现,但解释不同。
回到一幅静物图
观察素材 · AI 生成静物。用于比较画面中的形状、明暗、颜色与空间,不作为物理测量依据。
先看白球:左上方较亮,右侧逐渐变暗;再看桌面向右延伸的影子。两者一起支持“主要照明来自左上方”的判断。接着看盒子:顶面、正面与侧面的朝向不同,明暗也不同。
花瓶则没有只靠一条外轮廓来说明圆润。瓶身内部的过渡也参与塑造体积。叶片上的深色既可能与材料颜色有关,也会受照明影响;不能把不同物体的所有明暗差别都解释成光照。
这是生成的观察素材,可用于练习指出线索;它不用于验证光照数值。上面的几何实验负责展示受光与遮挡的确定关系。
换个物体:哪一步让罐子落到桌上?
左上箭头表示主光方向,三个罐子的材料相同。A 有悬浮感;B 和 C 都移动了投影。选一个既改善接触、又保留受光方向的版本。指出另外一个版本新增的问题。
先判断,再展开图解
选 B。投影与罐底接上,近处接触加深,原先左亮右暗的受光关系保留。C 虽然接上投影,却同时把左右明暗反了;在同材料、单一左上主光的条件下,这需要额外解释。不能因为“终于落地”就忽略另一处结构矛盾。此处是罐子而非杯子,检查方法仍相同。
用另一种情况检查理解
情境一: 光从右上方照向一个哑光盒子。有人把最亮的大面画在朝左的一侧,却没有补充第二个光源或反射条件。你会先检查什么?
展开参考思路
先检查面的朝向与光向是否一致,再区分材料本来就不同还是照明不同。如果各面材料相同且只有右上方的主光,朝左的面异常明亮就需要解释。不要只把它改成“左暗右亮”的口诀:顶面、正面与侧面各自朝哪里,才是判断依据。
情境二: 两张图的球面明暗相同,一张有地面投影,另一张没有。没有投影的球一定是错的吗?
展开参考思路
不一定。物体可以悬空,地面可能不在画框里,也可能采用平面化表达。但若两图都声称描述同一组照明和接触关系,就需要解释为什么删掉投影。先看图想表达什么,再检查所选线索是否相互支持。
带走一个观察方法
看一个物体时,依次问:轮廓是什么形状?表面明暗怎样随朝向变化?影子落在谁的表面上?这三个问题比寻找某一块“标准灰”更能帮助你读出体积。
可选实践:拿一个无光泽的杯子和一盏台灯,固定观看位置,只移动台灯。分别追踪杯子上的暗部和桌面的投影,不必画完整素描。
延伸阅读:Getty:用明暗表现体积、PBRT:漫反射模型。高光、反光和软硬影的机制可以在掌握主线后继续学习。